Merge pull request #5566 from lohit-bruno/crypto_safe_mode_support

fix crypto-js in safe mode
This commit is contained in:
Anoop M D
2025-09-16 16:11:05 +05:30
committed by GitHub
12 changed files with 379 additions and 32 deletions

9
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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
};
`;

View File

@@ -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 `

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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
}

View File

@@ -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');
});
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}