diff --git a/package-lock.json b/package-lock.json index 3d080c09d..cc46f5e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8739,12 +8739,6 @@ "resolved": "packages/bruno-converters", "link": true }, - "node_modules/@usebruno/crypto-js": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@usebruno/crypto-js/-/crypto-js-3.1.9.tgz", - "integrity": "sha512-khvEnRF6/UVDw4df06j+6lFWGNDYWlcWnxfmEgU2o/CdsGY291NC1Cexz99ud7sbGBQP2d8JUXZe4zXPkGNJpQ==", - "license": "MIT" - }, "node_modules/@usebruno/filestore": { "resolved": "packages/bruno-filestore", "link": true @@ -31758,7 +31752,6 @@ "license": "MIT", "dependencies": { "@usebruno/common": "0.1.0", - "@usebruno/crypto-js": "^3.1.9", "@usebruno/query": "0.1.0", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -31768,7 +31761,7 @@ "chai": "^4.3.7", "chai-string": "^1.5.0", "cheerio": "^1.0.0", - "crypto-js": "^4.1.1", + "crypto-js": "^4.2.0", "json-query": "^2.2.2", "lodash": "^4.17.21", "moment": "^2.29.4", diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index ae7749692..bcae171a4 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@usebruno/common": "0.1.0", - "@usebruno/crypto-js": "^3.1.9", "@usebruno/query": "0.1.0", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -26,7 +25,7 @@ "chai": "^4.3.7", "chai-string": "^1.5.0", "cheerio": "^1.0.0", - "crypto-js": "^4.1.1", + "crypto-js": "^4.2.0", "json-query": "^2.2.2", "lodash": "^4.17.21", "moment": "^2.29.4", @@ -45,4 +44,4 @@ "rollup": "3.29.5", "rollup-plugin-terser": "^7.0.2" } -} \ No newline at end of file +} diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index de8e37ba1..1b7263c67 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -131,7 +131,8 @@ class TestRuntime { if (this.runtime === 'quickjs') { await executeQuickJsVmAsync({ script: testsFile, - context: context + context: context, + collectionPath }); } else if (this.runtime === 'nodevm') { await runScriptInNodeVm({ @@ -147,6 +148,7 @@ class TestRuntime { require: { context: 'sandbox', external: true, + builtin: ['*'], root: [collectionPath, ...additionalContextRootsAbsolute], mock: { // node libs diff --git a/packages/bruno-js/src/sandbox/bundle-libraries.js b/packages/bruno-js/src/sandbox/bundle-libraries.js index 1545ef5cd..cd62ed710 100644 --- a/packages/bruno-js/src/sandbox/bundle-libraries.js +++ b/packages/bruno-js/src/sandbox/bundle-libraries.js @@ -11,7 +11,7 @@ const bundleLibraries = async () => { import moment from "moment"; import btoa from "btoa"; import atob from "atob"; - import * as CryptoJS from "@usebruno/crypto-js"; + import * as cryptoJs from 'crypto-js'; import tv4 from "tv4"; globalThis.expect = expect; globalThis.assert = assert; @@ -19,7 +19,6 @@ const bundleLibraries = async () => { globalThis.btoa = btoa; globalThis.atob = atob; globalThis.Buffer = Buffer; - globalThis.CryptoJS = CryptoJS; globalThis.tv4 = tv4; globalThis.requireObject = { ...(globalThis.requireObject || {}), @@ -28,7 +27,7 @@ const bundleLibraries = async () => { 'buffer': { Buffer }, 'btoa': btoa, 'atob': atob, - 'crypto-js': CryptoJS, + 'crypto-js': cryptoJs, 'tv4': tv4 }; `; diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js index d1340e92d..c95381a15 100644 --- a/packages/bruno-js/src/sandbox/quickjs/index.js +++ b/packages/bruno-js/src/sandbox/quickjs/index.js @@ -11,6 +11,7 @@ const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscrip const getBundledCode = require('../bundle-browser-rollup'); const addPathShimToContext = require('./shims/lib/path'); const { marshallToVm } = require('./utils'); +const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils'); let QuickJSSyncContext; const loader = memoizePromiseFactory(() => newQuickJSWASMModule()); @@ -98,6 +99,9 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external const module = await newQuickJSWASMModule(); const vm = module.newContext(); + // add crypto utilities required by the crypto-js library in bundledCode + await addCryptoUtilsShimToContext(vm); + const bundledCode = getBundledCode?.toString() || ''; const moduleLoaderCode = function () { return ` diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js new file mode 100644 index 000000000..50933d5fe --- /dev/null +++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js @@ -0,0 +1,104 @@ +const crypto = require('node:crypto'); +const { marshallToVm } = require('../../utils'); +const { serializeTypedArray, deserializeTypedArray } = require('./utils'); + +/** + * Node.js crypto module shim for QuickJS sandbox + * Implements crypto.randomBytes and crypto.getRandomValues functions + */ +const addCryptoUtilsShimToContext = async (vm) => { + let randomBytesHandle = vm.newFunction('randomBytes', function (sizeHandle) { + try { + let size = vm.dump(sizeHandle); + + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number'); + } + + size = Math.trunc(size); + + if (size < 0) { + throw new RangeError('The "size" argument must be >= 0'); + } + + if (size > 65536) { // 2^31 - 1 (max safe integer for practical use) + throw new RangeError('The "size" argument is too large'); + } + + if (size === 0) { + return marshallToVm([], vm); + } + + const buffer = crypto.randomBytes(size); + + const byteArray = Array.from(buffer); + + return marshallToVm(byteArray, vm); + + } catch (error) { + const vmError = vm.newError(error.message); + vm.setProp(vmError, 'name', vm.newString(error.name)); + + throw vmError; + } + }); + + let getRandomValuesHandle = vm.newFunction('getRandomValues', function (arrayHandle) { + try { + // Receive the serialized array data directly + const serializedArray = vm.dump(arrayHandle); + const typedArray = deserializeTypedArray(serializedArray); + + if (typedArray.length === 0) { + return marshallToVm([], vm); + } + + if (typedArray.length > 65536) { + throw new Error('getRandomValues: ArrayBufferView byte length exceeds 65536'); + } + + crypto.getRandomValues(typedArray); + + const byteArray = Array.from(typedArray); + + return marshallToVm(byteArray, vm); + + } catch (error) { + const vmError = vm.newError(error.message); + vm.setProp(vmError, 'name', vm.newString(error.name)); + + throw vmError; + } + }); + + // Set the functions in global context + vm.setProp(vm.global, '__bruno__crypto__randomBytes', randomBytesHandle); + vm.setProp(vm.global, '__bruno__crypto__getRandomValues', getRandomValuesHandle); + randomBytesHandle.dispose(); + getRandomValuesHandle.dispose(); + + vm.evalCode(` + // Helper function for typed array serialization + ${serializeTypedArray.toString()} + + // Create crypto module object following Node.js specifications + const cryptoModule = { + // node.js crypto.randomBytes API + randomBytes: function(size) { + const byteArray = globalThis.__bruno__crypto__randomBytes(size); + return Buffer.from(Array.from(byteArray)); + }, + // node.js crypto.getRandomValues API + getRandomValues: function(typedArray) { + const serializedTypedArray = serializeTypedArray(typedArray); + typedArray.set(globalThis.__bruno__crypto__getRandomValues(serializedTypedArray)); + return typedArray; + }, + }; + + // Make crypto available globally + globalThis.crypto = cryptoModule; + `); +}; + +module.exports = addCryptoUtilsShimToContext; \ No newline at end of file diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js new file mode 100644 index 000000000..969ef514d --- /dev/null +++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js @@ -0,0 +1,73 @@ +const { describe, it, expect } = require('@jest/globals'); +const { newQuickJSWASMModule } = require('quickjs-emscripten'); +const addCryptoUtilsShimToContext = require('./crypto-utils'); +const getBundledCode = require('../../../bundle-browser-rollup'); + +describe('crypto-utils shims tests', () => { + let vm, module; + + beforeAll(async () => { + module = await newQuickJSWASMModule(); + }); + + beforeEach(async () => { + vm = module.newContext(); + await addCryptoUtilsShimToContext(vm); + // required for `Buffer` library usage + const bundledCode = getBundledCode?.toString() || ''; + vm.evalCode( + ` + (${bundledCode})() + ` + ); + }); + + it('should provide crypto.randomBytes function', async () => { + const result = vm.evalCode('typeof crypto.randomBytes'); + const handle = vm.unwrapResult(result); + const type = vm.dump(handle); + handle.dispose(); + + expect(type).toBe('function'); + }); + + it('should provide crypto.getRandomValues function', async () => { + const result = vm.evalCode('typeof crypto.getRandomValues'); + const handle = vm.unwrapResult(result); + const type = vm.dump(handle); + handle.dispose(); + + expect(type).toBe('function'); + }); + + it('should generate random bytes with correct length', async () => { + const result = vm.evalCode('crypto.randomBytes(8).length'); + const handle = vm.unwrapResult(result); + const length = vm.dump(handle); + handle.dispose(); + + expect(length).toBe(8); + }); + + it('should convert random bytes to hex string', async () => { + const result = vm.evalCode('crypto.randomBytes(4).toString("hex").length'); + const handle = vm.unwrapResult(result); + const hexLength = vm.dump(handle); + handle.dispose(); + + expect(hexLength).toBe(8); // 4 bytes = 8 hex chars + }); + + it('should fill Uint8Array with getRandomValues', async () => { + const result = vm.evalCode(` + const arr = new Uint8Array(5); + crypto.getRandomValues(arr); + arr.length; + `); + const handle = vm.unwrapResult(result); + const length = vm.dump(handle); + handle.dispose(); + + expect(length).toBe(5); + }); +}); \ No newline at end of file diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js new file mode 100644 index 000000000..757d14c52 --- /dev/null +++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js @@ -0,0 +1,48 @@ +function serializeTypedArray(ta) { + return { + type: ta.constructor.name, + array: Array.from(ta), + length: ta.length + }; +} + +function deserializeTypedArray(obj) { + // Allowed typed array constructors for crypto operations + const allowedConstructors = new Set([ + 'Int8Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Int16Array', + 'Uint16Array', + 'Int32Array', + 'Uint32Array', + 'Float32Array', + 'Float64Array', + 'BigInt64Array', + 'BigUint64Array' + ]); + + if (!obj || typeof obj !== 'object') { + throw new TypeError('getRandomValues: Invalid typed array object'); + } + + if (typeof obj.type !== 'string' || !allowedConstructors.has(obj.type)) { + throw new TypeError(`getRandomValues: Invalid or unsupported typed array type: ${obj.type}`); + } + + if (!obj.array || typeof obj.length !== 'number') { + throw new TypeError('getRandomValues: Invalid typed array properties'); + } + + const ctor = globalThis[obj.type]; + if (typeof ctor !== 'function') { + throw new TypeError(`getRandomValues: Constructor ${obj.type} is not available`); + } + + return new ctor(obj.array, 0, obj.length); +} + +module.exports = { + serializeTypedArray, + deserializeTypedArray +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru index 8385847c9..30a784bc3 100644 --- a/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru @@ -10,24 +10,17 @@ get { auth: none } -script:pre-request { - var CryptoJS = require("crypto-js"); - - // Encrypt - var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString(); - - // Decrypt - var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123'); - var originalText = bytes.toString(CryptoJS.enc.Utf8); - - bru.setVar('crypto-test-message', originalText); -} - tests { test("crypto message", function() { - const data = bru.getVar('crypto-test-message'); - bru.setVar('crypto-test-message', null); - expect(data).to.eql('my message'); + var CryptoJS = require("crypto-js"); + + // Encrypt + var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString(); + + // Decrypt + var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123'); + var originalText = bytes.toString(CryptoJS.enc.Utf8); + + expect(originalText).to.eql('my message'); }); - } diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/getRandomValues.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/getRandomValues.bru new file mode 100644 index 000000000..c07e30530 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/getRandomValues.bru @@ -0,0 +1,43 @@ +meta { + name: getRandomValues + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: none + auth: inherit +} + +assert { + res.status: eq 200 +} + +tests { + const { doesUint8ArraysWorkAsExpected, getRandomValuesFunction, isUint8Array } = require('./scripting/inbuilt modules/utils.js'); + + if (!doesUint8ArraysWorkAsExpected()) { + console.warn('Uint8Array does not work as expected in vm2'); + return; + } + + // check if Uint8Array work as expected + test("should get random values", function() { + const uint8Array = new Uint8Array(32).fill(0); + const randomValueUint8Array = getRandomValuesFunction(new Uint8Array(uint8Array)); + + const isValueUint8Array = isUint8Array(randomValueUint8Array); + expect(isValueUint8Array).to.be.true; + + const plainArray = Array.from(randomValueUint8Array); + expect(plainArray).to.be.of.length(32); + + const ogPlainArray = Array.from(uint8Array); + expect(ogPlainArray).to.not.deep.eql(plainArray); + }); +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/randomBytes.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/randomBytes.bru new file mode 100644 index 000000000..356f8de1c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/randomBytes.bru @@ -0,0 +1,33 @@ +meta { + name: randomBytes + type: http + seq: 4 +} + +post { + url: https://echo.usebruno.com + body: none + auth: inherit +} + +assert { + res.status: eq 200 +} + +tests { + const { randomBytesFunction, isUint8Array } = require('./scripting/inbuilt modules/utils.js'); + + test("should get random byte values", function() { + const randomValueUint8Array = randomBytesFunction(32); + + const isValueUint8Array = isUint8Array(randomValueUint8Array); + expect(isValueUint8Array).to.be.true; + + const plainArray = Array.from(randomValueUint8Array); + expect(plainArray).to.be.of.length(32); + }); +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/utils.js b/packages/bruno-tests/collection/scripting/inbuilt modules/utils.js new file mode 100644 index 000000000..5b9b86174 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/utils.js @@ -0,0 +1,56 @@ +const doesUint8ArraysWorkAsExpected = () => { + try { + const util = require('node:util'); + // node:vm - true + // vm2 - false + return util.types.isUint8Array(new Uint8Array(32)); + } + catch (err) { + // safe mode [quickjs], will work as expected + return true; + } +} + +const isUint8Array = (val) => { + try { + // developer mode [node:vm and vm2] + const util = require('node:util'); + return util.types.isUint8Array(val); + } + catch (err) { + // node:util not present in safe mode [quickjs] + return val instanceof Uint8Array; + } +} + +const getRandomValuesFunction = (typedArray) => { + try { + // developer mode [node:vm and vm2] + const crypto = require('node:crypto'); + return crypto.getRandomValues(typedArray); + } + catch (err) { + // node:crypto not present in safe mode [quickjs] - uses shim + return crypto.getRandomValues(typedArray); + } +} + +const randomBytesFunction = (num) => { + try { + // developer mode [node:vm and vm2] + const crypto = require('node:crypto'); + return crypto.randomBytes(num); + } + catch (err) { + // node:crypto not present in safe mode [quickjs] - uses shim + return crypto.randomBytes(num); + } +} + + +module.exports = { + doesUint8ArraysWorkAsExpected, + isUint8Array, + getRandomValuesFunction, + randomBytesFunction +} \ No newline at end of file