mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-12 10:21:30 +00:00
Merge pull request #5566 from lohit-bruno/crypto_safe_mode_support
fix crypto-js in safe mode
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -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 `
|
||||
|
||||
104
packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js
Normal file
104
packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
48
packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js
Normal file
48
packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js
Normal 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
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user