@usebruno/js 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -5
- package/src/index.js +3 -1
- package/src/runtime/script-runtime.js +45 -0
- package/src/runtime/test-runtime.js +11 -1
- package/src/sandbox/bundle-libraries.js +2 -3
- package/src/sandbox/node-vm/index.js +223 -0
- package/src/sandbox/quickjs/index.js +4 -0
- package/src/sandbox/quickjs/shims/lib/crypto-utils.js +104 -0
- package/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js +73 -0
- package/src/sandbox/quickjs/shims/lib/utils.js +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"prepack": "npm run test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@usebruno/common": "0.
|
|
20
|
-
"@usebruno/crypto-js": "^3.1.9",
|
|
19
|
+
"@usebruno/common": "0.12.1",
|
|
21
20
|
"@usebruno/query": "0.1.0",
|
|
22
21
|
"ajv": "^8.12.0",
|
|
23
22
|
"ajv-formats": "^2.1.1",
|
|
@@ -27,7 +26,7 @@
|
|
|
27
26
|
"chai": "^4.3.7",
|
|
28
27
|
"chai-string": "^1.5.0",
|
|
29
28
|
"cheerio": "^1.0.0",
|
|
30
|
-
"crypto-js": "^4.
|
|
29
|
+
"crypto-js": "^4.2.0",
|
|
31
30
|
"json-query": "^2.2.2",
|
|
32
31
|
"lodash": "^4.17.21",
|
|
33
32
|
"moment": "^2.29.4",
|
|
@@ -46,4 +45,4 @@
|
|
|
46
45
|
"rollup": "3.29.5",
|
|
47
46
|
"rollup-plugin-terser": "^7.0.2"
|
|
48
47
|
}
|
|
49
|
-
}
|
|
48
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,12 @@ const ScriptRuntime = require('./runtime/script-runtime');
|
|
|
2
2
|
const TestRuntime = require('./runtime/test-runtime');
|
|
3
3
|
const VarsRuntime = require('./runtime/vars-runtime');
|
|
4
4
|
const AssertRuntime = require('./runtime/assert-runtime');
|
|
5
|
+
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
ScriptRuntime,
|
|
8
9
|
TestRuntime,
|
|
9
10
|
VarsRuntime,
|
|
10
|
-
AssertRuntime
|
|
11
|
+
AssertRuntime,
|
|
12
|
+
runScriptInNodeVm
|
|
11
13
|
};
|
|
@@ -14,6 +14,7 @@ const BrunoRequest = require('../bruno-request');
|
|
|
14
14
|
const BrunoResponse = require('../bruno-response');
|
|
15
15
|
const { cleanJson } = require('../utils');
|
|
16
16
|
const { createBruTestResultMethods } = require('../utils/results');
|
|
17
|
+
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
17
18
|
|
|
18
19
|
// Inbuilt Library Support
|
|
19
20
|
const ajv = require('ajv');
|
|
@@ -119,6 +120,28 @@ class ScriptRuntime {
|
|
|
119
120
|
context.bru.runRequest = runRequestByItemPathname;
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
if (this.runtime === 'nodevm') {
|
|
124
|
+
await runScriptInNodeVm({
|
|
125
|
+
script,
|
|
126
|
+
context,
|
|
127
|
+
collectionPath,
|
|
128
|
+
scriptingConfig
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
request,
|
|
133
|
+
envVariables: cleanJson(envVariables),
|
|
134
|
+
runtimeVariables: cleanJson(runtimeVariables),
|
|
135
|
+
visualizations,
|
|
136
|
+
persistentEnvVariables: bru.persistentEnvVariables,
|
|
137
|
+
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
138
|
+
results: cleanJson(__brunoTestResults.getResults()),
|
|
139
|
+
nextRequestName: bru.nextRequest,
|
|
140
|
+
skipRequest: bru.skipRequest,
|
|
141
|
+
stopExecution: bru.stopExecution
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
122
145
|
if (this.runtime === 'quickjs') {
|
|
123
146
|
await executeQuickJsVmAsync({
|
|
124
147
|
script: script,
|
|
@@ -277,6 +300,28 @@ class ScriptRuntime {
|
|
|
277
300
|
context.bru.runRequest = runRequestByItemPathname;
|
|
278
301
|
}
|
|
279
302
|
|
|
303
|
+
if (this.runtime === 'nodevm') {
|
|
304
|
+
await runScriptInNodeVm({
|
|
305
|
+
script,
|
|
306
|
+
context,
|
|
307
|
+
collectionPath,
|
|
308
|
+
scriptingConfig
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
response,
|
|
313
|
+
envVariables: cleanJson(envVariables),
|
|
314
|
+
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
315
|
+
runtimeVariables: cleanJson(runtimeVariables),
|
|
316
|
+
visualizations,
|
|
317
|
+
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
318
|
+
results: cleanJson(__brunoTestResults.getResults()),
|
|
319
|
+
nextRequestName: bru.nextRequest,
|
|
320
|
+
skipRequest: bru.skipRequest,
|
|
321
|
+
stopExecution: bru.stopExecution
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
280
325
|
if (this.runtime === 'quickjs') {
|
|
281
326
|
await executeQuickJsVmAsync({
|
|
282
327
|
script: script,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { NodeVM } = require('@usebruno/vm2');
|
|
2
|
+
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
2
3
|
const chai = require('chai');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const http = require('http');
|
|
@@ -134,7 +135,15 @@ class TestRuntime {
|
|
|
134
135
|
if (this.runtime === 'quickjs') {
|
|
135
136
|
await executeQuickJsVmAsync({
|
|
136
137
|
script: testsFile,
|
|
137
|
-
context: context
|
|
138
|
+
context: context,
|
|
139
|
+
collectionPath
|
|
140
|
+
});
|
|
141
|
+
} else if (this.runtime === 'nodevm') {
|
|
142
|
+
await runScriptInNodeVm({
|
|
143
|
+
script: testsFile,
|
|
144
|
+
context,
|
|
145
|
+
collectionPath,
|
|
146
|
+
scriptingConfig
|
|
138
147
|
});
|
|
139
148
|
} else {
|
|
140
149
|
// default runtime is vm2
|
|
@@ -143,6 +152,7 @@ class TestRuntime {
|
|
|
143
152
|
require: {
|
|
144
153
|
context: 'sandbox',
|
|
145
154
|
external: true,
|
|
155
|
+
builtin: ['*'],
|
|
146
156
|
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
147
157
|
mock: {
|
|
148
158
|
// node libs
|
|
@@ -11,7 +11,7 @@ const bundleLibraries = async () => {
|
|
|
11
11
|
import moment from "moment";
|
|
12
12
|
import btoa from "btoa";
|
|
13
13
|
import atob from "atob";
|
|
14
|
-
import * as
|
|
14
|
+
import * as cryptoJs from 'crypto-js';
|
|
15
15
|
import tv4 from "tv4";
|
|
16
16
|
globalThis.expect = expect;
|
|
17
17
|
globalThis.assert = assert;
|
|
@@ -19,7 +19,6 @@ const bundleLibraries = async () => {
|
|
|
19
19
|
globalThis.btoa = btoa;
|
|
20
20
|
globalThis.atob = atob;
|
|
21
21
|
globalThis.Buffer = Buffer;
|
|
22
|
-
globalThis.CryptoJS = CryptoJS;
|
|
23
22
|
globalThis.tv4 = tv4;
|
|
24
23
|
globalThis.requireObject = {
|
|
25
24
|
...(globalThis.requireObject || {}),
|
|
@@ -28,7 +27,7 @@ const bundleLibraries = async () => {
|
|
|
28
27
|
'buffer': { Buffer },
|
|
29
28
|
'btoa': btoa,
|
|
30
29
|
'atob': atob,
|
|
31
|
-
'crypto-js':
|
|
30
|
+
'crypto-js': cryptoJs,
|
|
32
31
|
'tv4': tv4
|
|
33
32
|
};
|
|
34
33
|
`;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const vm = require('node:vm');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { get } = require('lodash');
|
|
5
|
+
const lodash = require('lodash');
|
|
6
|
+
const { cleanJson } = require('../../utils');
|
|
7
|
+
|
|
8
|
+
class ScriptError extends Error {
|
|
9
|
+
constructor(error, script) {
|
|
10
|
+
super(error.message);
|
|
11
|
+
this.name = 'ScriptError';
|
|
12
|
+
this.originalError = error;
|
|
13
|
+
this.script = script;
|
|
14
|
+
this.stack = error.stack;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Executes a script in a Node.js VM context with enhanced security and module loading
|
|
20
|
+
* @param {Object} options - Configuration options
|
|
21
|
+
* @param {string} options.script - The script code to execute
|
|
22
|
+
* @param {Object} options.context - The execution context with Bruno objects
|
|
23
|
+
* @param {string} options.collectionPath - Path to the collection directory
|
|
24
|
+
* @param {Object} options.scriptingConfig - Scripting configuration options
|
|
25
|
+
* @returns {Promise<Object>} Execution results including variables and test results
|
|
26
|
+
* @throws {ScriptError} When script execution fails
|
|
27
|
+
*/
|
|
28
|
+
async function runScriptInNodeVm({
|
|
29
|
+
script,
|
|
30
|
+
context,
|
|
31
|
+
collectionPath,
|
|
32
|
+
scriptingConfig
|
|
33
|
+
}) {
|
|
34
|
+
if (script.trim().length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Create script context with all necessary variables
|
|
40
|
+
const scriptContext = {
|
|
41
|
+
// Bruno context
|
|
42
|
+
console: context.console,
|
|
43
|
+
req: context.req,
|
|
44
|
+
res: context.res,
|
|
45
|
+
bru: context.bru,
|
|
46
|
+
expect: context.expect,
|
|
47
|
+
assert: context.assert,
|
|
48
|
+
__brunoTestResults: context.__brunoTestResults,
|
|
49
|
+
test: context.test,
|
|
50
|
+
// Configuration for nested module loading
|
|
51
|
+
scriptingConfig: scriptingConfig,
|
|
52
|
+
// Global objects
|
|
53
|
+
Buffer: global.Buffer,
|
|
54
|
+
process: global.process,
|
|
55
|
+
setTimeout: global.setTimeout,
|
|
56
|
+
setInterval: global.setInterval,
|
|
57
|
+
clearTimeout: global.clearTimeout,
|
|
58
|
+
clearInterval: global.clearInterval,
|
|
59
|
+
setImmediate: global.setImmediate,
|
|
60
|
+
clearImmediate: global.clearImmediate
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Create shared cache for local modules
|
|
64
|
+
const localModuleCache = new Map();
|
|
65
|
+
|
|
66
|
+
// Create a custom require function and add it to the context
|
|
67
|
+
scriptContext.require = createCustomRequire({
|
|
68
|
+
scriptingConfig,
|
|
69
|
+
collectionPath,
|
|
70
|
+
scriptContext,
|
|
71
|
+
currentModuleDir: collectionPath,
|
|
72
|
+
localModuleCache
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Execute the script in an isolated VM context
|
|
76
|
+
await vm.runInNewContext(`
|
|
77
|
+
(async function(){
|
|
78
|
+
${script}
|
|
79
|
+
})();
|
|
80
|
+
`, scriptContext, {
|
|
81
|
+
filename: path.join(collectionPath, 'script.js'),
|
|
82
|
+
displayErrors: true
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new ScriptError(error, script);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a custom require function with enhanced security and local module support
|
|
94
|
+
* @param {Object} options - Configuration options
|
|
95
|
+
* @param {Object} options.scriptingConfig - Scripting configuration with additional context roots
|
|
96
|
+
* @param {string} options.collectionPath - Base collection path for security checks
|
|
97
|
+
* @param {Object} options.scriptContext - Script execution context
|
|
98
|
+
* @param {string} options.currentModuleDir - Current module directory for relative imports
|
|
99
|
+
* @param {Map} options.localModuleCache - Cache for loaded local modules
|
|
100
|
+
* @returns {Function} Custom require function
|
|
101
|
+
*/
|
|
102
|
+
function createCustomRequire({
|
|
103
|
+
scriptingConfig,
|
|
104
|
+
collectionPath,
|
|
105
|
+
scriptContext,
|
|
106
|
+
currentModuleDir = collectionPath,
|
|
107
|
+
localModuleCache = new Map()
|
|
108
|
+
}) {
|
|
109
|
+
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
110
|
+
const additionalContextRootsAbsolute = lodash
|
|
111
|
+
.chain(additionalContextRoots)
|
|
112
|
+
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
113
|
+
.value();
|
|
114
|
+
additionalContextRootsAbsolute.push(collectionPath);
|
|
115
|
+
|
|
116
|
+
return (moduleName) => {
|
|
117
|
+
// Check if it's a local module (starts with ./ or ../)
|
|
118
|
+
if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
|
|
119
|
+
return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// First try to require as a native/npm module
|
|
123
|
+
try {
|
|
124
|
+
return require(moduleName);
|
|
125
|
+
} catch {
|
|
126
|
+
// If that fails, try to resolve from additionalContextRoots
|
|
127
|
+
try {
|
|
128
|
+
const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
|
|
129
|
+
return require(modulePath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Loads a local module from the filesystem with security checks and caching
|
|
139
|
+
* @param {Object} options - Configuration options
|
|
140
|
+
* @param {string} options.moduleName - Name/path of the module to load
|
|
141
|
+
* @param {string} options.collectionPath - Base collection path for security validation
|
|
142
|
+
* @param {Object} options.scriptContext - Script execution context to inherit
|
|
143
|
+
* @param {Map} options.localModuleCache - Cache for loaded modules
|
|
144
|
+
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
|
|
145
|
+
* @returns {*} The exported content of the loaded module
|
|
146
|
+
* @throws {Error} When module is outside collection path or cannot be loaded
|
|
147
|
+
*/
|
|
148
|
+
function loadLocalModule({
|
|
149
|
+
moduleName,
|
|
150
|
+
collectionPath,
|
|
151
|
+
scriptContext,
|
|
152
|
+
localModuleCache,
|
|
153
|
+
currentModuleDir
|
|
154
|
+
}) {
|
|
155
|
+
// Check if the filename has an extension
|
|
156
|
+
const hasExtension = path.extname(moduleName) !== '';
|
|
157
|
+
const resolvedFilename = hasExtension ? moduleName : `${moduleName}.js`;
|
|
158
|
+
|
|
159
|
+
// Resolve the file path relative to the current module's directory
|
|
160
|
+
const filePath = path.resolve(currentModuleDir, resolvedFilename);
|
|
161
|
+
const normalizedFilePath = path.normalize(filePath);
|
|
162
|
+
const normalizedCollectionPath = path.normalize(collectionPath);
|
|
163
|
+
|
|
164
|
+
// Cross-platform security check: ensure the resolved file is within collectionPath
|
|
165
|
+
const relativePath = path.relative(normalizedCollectionPath, normalizedFilePath);
|
|
166
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
167
|
+
throw new Error(`Access to files outside of the collectionPath is not allowed: ${moduleName}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check cache first (use normalized path as key)
|
|
171
|
+
if (localModuleCache.has(normalizedFilePath)) {
|
|
172
|
+
return localModuleCache.get(normalizedFilePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(normalizedFilePath)) {
|
|
176
|
+
throw new Error(`Cannot find module ${moduleName}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Read and execute the local module
|
|
180
|
+
const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
|
|
181
|
+
|
|
182
|
+
// Create module object
|
|
183
|
+
const moduleObj = { exports: {} };
|
|
184
|
+
|
|
185
|
+
// Get the directory of this module for nested imports
|
|
186
|
+
const moduleDir = path.dirname(normalizedFilePath);
|
|
187
|
+
|
|
188
|
+
// Create a new context that inherits from the script context
|
|
189
|
+
const moduleContext = {
|
|
190
|
+
...scriptContext,
|
|
191
|
+
module: moduleObj,
|
|
192
|
+
exports: moduleObj.exports,
|
|
193
|
+
__filename: normalizedFilePath,
|
|
194
|
+
__dirname: moduleDir,
|
|
195
|
+
// Create a custom require function for this module that resolves relative to its directory
|
|
196
|
+
require: createCustomRequire({
|
|
197
|
+
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
198
|
+
collectionPath,
|
|
199
|
+
scriptContext,
|
|
200
|
+
currentModuleDir: moduleDir,
|
|
201
|
+
localModuleCache
|
|
202
|
+
})
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Execute the module code in the shared context
|
|
207
|
+
vm.runInNewContext(moduleCode, moduleContext, {
|
|
208
|
+
filename: normalizedFilePath,
|
|
209
|
+
displayErrors: true
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Cache the result using normalized path
|
|
213
|
+
localModuleCache.set(normalizedFilePath, moduleObj.exports);
|
|
214
|
+
|
|
215
|
+
return moduleObj.exports;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
runScriptInNodeVm
|
|
223
|
+
};
|
|
@@ -11,6 +11,7 @@ const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscrip
|
|
|
11
11
|
const getBundledCode = require('../bundle-browser-rollup');
|
|
12
12
|
const addPathShimToContext = require('./shims/lib/path');
|
|
13
13
|
const { marshallToVm } = require('./utils');
|
|
14
|
+
const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
|
|
14
15
|
|
|
15
16
|
let QuickJSSyncContext;
|
|
16
17
|
const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
|
|
@@ -98,6 +99,9 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
98
99
|
const module = await newQuickJSWASMModule();
|
|
99
100
|
const vm = module.newContext();
|
|
100
101
|
|
|
102
|
+
// add crypto utilities required by the crypto-js library in bundledCode
|
|
103
|
+
await addCryptoUtilsShimToContext(vm);
|
|
104
|
+
|
|
101
105
|
const bundledCode = getBundledCode?.toString() || '';
|
|
102
106
|
const moduleLoaderCode = function () {
|
|
103
107
|
return `
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const { marshallToVm } = require('../../utils');
|
|
3
|
+
const { serializeTypedArray, deserializeTypedArray } = require('./utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node.js crypto module shim for QuickJS sandbox
|
|
7
|
+
* Implements crypto.randomBytes and crypto.getRandomValues functions
|
|
8
|
+
*/
|
|
9
|
+
const addCryptoUtilsShimToContext = async (vm) => {
|
|
10
|
+
let randomBytesHandle = vm.newFunction('randomBytes', function (sizeHandle) {
|
|
11
|
+
try {
|
|
12
|
+
let size = vm.dump(sizeHandle);
|
|
13
|
+
|
|
14
|
+
if (typeof size !== 'number') {
|
|
15
|
+
throw new TypeError('The "size" argument must be of type number');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
size = Math.trunc(size);
|
|
19
|
+
|
|
20
|
+
if (size < 0) {
|
|
21
|
+
throw new RangeError('The "size" argument must be >= 0');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (size > 65536) { // 2^31 - 1 (max safe integer for practical use)
|
|
25
|
+
throw new RangeError('The "size" argument is too large');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (size === 0) {
|
|
29
|
+
return marshallToVm([], vm);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const buffer = crypto.randomBytes(size);
|
|
33
|
+
|
|
34
|
+
const byteArray = Array.from(buffer);
|
|
35
|
+
|
|
36
|
+
return marshallToVm(byteArray, vm);
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const vmError = vm.newError(error.message);
|
|
40
|
+
vm.setProp(vmError, 'name', vm.newString(error.name));
|
|
41
|
+
|
|
42
|
+
throw vmError;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let getRandomValuesHandle = vm.newFunction('getRandomValues', function (arrayHandle) {
|
|
47
|
+
try {
|
|
48
|
+
// Receive the serialized array data directly
|
|
49
|
+
const serializedArray = vm.dump(arrayHandle);
|
|
50
|
+
const typedArray = deserializeTypedArray(serializedArray);
|
|
51
|
+
|
|
52
|
+
if (typedArray.length === 0) {
|
|
53
|
+
return marshallToVm([], vm);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typedArray.length > 65536) {
|
|
57
|
+
throw new Error('getRandomValues: ArrayBufferView byte length exceeds 65536');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
crypto.getRandomValues(typedArray);
|
|
61
|
+
|
|
62
|
+
const byteArray = Array.from(typedArray);
|
|
63
|
+
|
|
64
|
+
return marshallToVm(byteArray, vm);
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const vmError = vm.newError(error.message);
|
|
68
|
+
vm.setProp(vmError, 'name', vm.newString(error.name));
|
|
69
|
+
|
|
70
|
+
throw vmError;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Set the functions in global context
|
|
75
|
+
vm.setProp(vm.global, '__bruno__crypto__randomBytes', randomBytesHandle);
|
|
76
|
+
vm.setProp(vm.global, '__bruno__crypto__getRandomValues', getRandomValuesHandle);
|
|
77
|
+
randomBytesHandle.dispose();
|
|
78
|
+
getRandomValuesHandle.dispose();
|
|
79
|
+
|
|
80
|
+
vm.evalCode(`
|
|
81
|
+
// Helper function for typed array serialization
|
|
82
|
+
${serializeTypedArray.toString()}
|
|
83
|
+
|
|
84
|
+
// Create crypto module object following Node.js specifications
|
|
85
|
+
const cryptoModule = {
|
|
86
|
+
// node.js crypto.randomBytes API
|
|
87
|
+
randomBytes: function(size) {
|
|
88
|
+
const byteArray = globalThis.__bruno__crypto__randomBytes(size);
|
|
89
|
+
return Buffer.from(Array.from(byteArray));
|
|
90
|
+
},
|
|
91
|
+
// node.js crypto.getRandomValues API
|
|
92
|
+
getRandomValues: function(typedArray) {
|
|
93
|
+
const serializedTypedArray = serializeTypedArray(typedArray);
|
|
94
|
+
typedArray.set(globalThis.__bruno__crypto__getRandomValues(serializedTypedArray));
|
|
95
|
+
return typedArray;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Make crypto available globally
|
|
100
|
+
globalThis.crypto = cryptoModule;
|
|
101
|
+
`);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = addCryptoUtilsShimToContext;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { describe, it, expect } = require('@jest/globals');
|
|
2
|
+
const { newQuickJSWASMModule } = require('quickjs-emscripten');
|
|
3
|
+
const addCryptoUtilsShimToContext = require('./crypto-utils');
|
|
4
|
+
const getBundledCode = require('../../../bundle-browser-rollup');
|
|
5
|
+
|
|
6
|
+
describe('crypto-utils shims tests', () => {
|
|
7
|
+
let vm, module;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
module = await newQuickJSWASMModule();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
vm = module.newContext();
|
|
15
|
+
await addCryptoUtilsShimToContext(vm);
|
|
16
|
+
// required for `Buffer` library usage
|
|
17
|
+
const bundledCode = getBundledCode?.toString() || '';
|
|
18
|
+
vm.evalCode(
|
|
19
|
+
`
|
|
20
|
+
(${bundledCode})()
|
|
21
|
+
`
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should provide crypto.randomBytes function', async () => {
|
|
26
|
+
const result = vm.evalCode('typeof crypto.randomBytes');
|
|
27
|
+
const handle = vm.unwrapResult(result);
|
|
28
|
+
const type = vm.dump(handle);
|
|
29
|
+
handle.dispose();
|
|
30
|
+
|
|
31
|
+
expect(type).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should provide crypto.getRandomValues function', async () => {
|
|
35
|
+
const result = vm.evalCode('typeof crypto.getRandomValues');
|
|
36
|
+
const handle = vm.unwrapResult(result);
|
|
37
|
+
const type = vm.dump(handle);
|
|
38
|
+
handle.dispose();
|
|
39
|
+
|
|
40
|
+
expect(type).toBe('function');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should generate random bytes with correct length', async () => {
|
|
44
|
+
const result = vm.evalCode('crypto.randomBytes(8).length');
|
|
45
|
+
const handle = vm.unwrapResult(result);
|
|
46
|
+
const length = vm.dump(handle);
|
|
47
|
+
handle.dispose();
|
|
48
|
+
|
|
49
|
+
expect(length).toBe(8);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should convert random bytes to hex string', async () => {
|
|
53
|
+
const result = vm.evalCode('crypto.randomBytes(4).toString("hex").length');
|
|
54
|
+
const handle = vm.unwrapResult(result);
|
|
55
|
+
const hexLength = vm.dump(handle);
|
|
56
|
+
handle.dispose();
|
|
57
|
+
|
|
58
|
+
expect(hexLength).toBe(8); // 4 bytes = 8 hex chars
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should fill Uint8Array with getRandomValues', async () => {
|
|
62
|
+
const result = vm.evalCode(`
|
|
63
|
+
const arr = new Uint8Array(5);
|
|
64
|
+
crypto.getRandomValues(arr);
|
|
65
|
+
arr.length;
|
|
66
|
+
`);
|
|
67
|
+
const handle = vm.unwrapResult(result);
|
|
68
|
+
const length = vm.dump(handle);
|
|
69
|
+
handle.dispose();
|
|
70
|
+
|
|
71
|
+
expect(length).toBe(5);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function serializeTypedArray(ta) {
|
|
2
|
+
return {
|
|
3
|
+
type: ta.constructor.name,
|
|
4
|
+
array: Array.from(ta),
|
|
5
|
+
length: ta.length
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function deserializeTypedArray(obj) {
|
|
10
|
+
// Allowed typed array constructors for crypto operations
|
|
11
|
+
const allowedConstructors = new Set([
|
|
12
|
+
'Int8Array',
|
|
13
|
+
'Uint8Array',
|
|
14
|
+
'Uint8ClampedArray',
|
|
15
|
+
'Int16Array',
|
|
16
|
+
'Uint16Array',
|
|
17
|
+
'Int32Array',
|
|
18
|
+
'Uint32Array',
|
|
19
|
+
'Float32Array',
|
|
20
|
+
'Float64Array',
|
|
21
|
+
'BigInt64Array',
|
|
22
|
+
'BigUint64Array'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
if (!obj || typeof obj !== 'object') {
|
|
26
|
+
throw new TypeError('getRandomValues: Invalid typed array object');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof obj.type !== 'string' || !allowedConstructors.has(obj.type)) {
|
|
30
|
+
throw new TypeError(`getRandomValues: Invalid or unsupported typed array type: ${obj.type}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!obj.array || typeof obj.length !== 'number') {
|
|
34
|
+
throw new TypeError('getRandomValues: Invalid typed array properties');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ctor = globalThis[obj.type];
|
|
38
|
+
if (typeof ctor !== 'function') {
|
|
39
|
+
throw new TypeError(`getRandomValues: Constructor ${obj.type} is not available`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new ctor(obj.array, 0, obj.length);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
serializeTypedArray,
|
|
47
|
+
deserializeTypedArray
|
|
48
|
+
}
|