seabox 0.1.0-beta.1
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/.mocharc.json +6 -0
- package/LICENSE.MD +21 -0
- package/README.md +209 -0
- package/bin/seabox-rebuild.js +395 -0
- package/bin/seabox.js +81 -0
- package/lib/bindings.js +31 -0
- package/lib/blob.js +109 -0
- package/lib/bootstrap.js +655 -0
- package/lib/build.js +283 -0
- package/lib/config.js +117 -0
- package/lib/crypto-assets.js +160 -0
- package/lib/fetch-node.js +177 -0
- package/lib/index.js +27 -0
- package/lib/inject.js +81 -0
- package/lib/manifest.js +98 -0
- package/lib/obfuscate.js +73 -0
- package/lib/scanner.js +153 -0
- package/lib/unsign.js +184 -0
- package/package.json +47 -0
package/lib/build.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.js
|
|
3
|
+
* Main orchestrator for SEA build pipeline.
|
|
4
|
+
* Usage: node scripts/sea/build.js [--config <path>] [--verbose]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { loadConfig, parseTarget } = require('./config');
|
|
10
|
+
const { scanAssets, groupAssets } = require('./scanner');
|
|
11
|
+
const { generateManifest, serializeManifest } = require('./manifest');
|
|
12
|
+
const { createSeaConfig, writeSeaConfigJson, generateBlob } = require('./blob');
|
|
13
|
+
const { fetchNodeBinary } = require('./fetch-node');
|
|
14
|
+
const { injectBlob } = require('./inject');
|
|
15
|
+
const { generateEncryptionKey, encryptAssets, keyToObfuscatedCode } = require('./crypto-assets');
|
|
16
|
+
const { obfuscateBootstrap } = require('./obfuscate');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Main build orchestrator.
|
|
21
|
+
* @param {Object} options - Build options
|
|
22
|
+
* @param {string} [options.configPath] - Path to config file
|
|
23
|
+
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
24
|
+
* @param {boolean} [options.debug] - Keep temporary build files
|
|
25
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
26
|
+
*/
|
|
27
|
+
async function build(options = {}) {
|
|
28
|
+
// Support both options object and legacy process.argv parsing
|
|
29
|
+
let configPath = options.configPath;
|
|
30
|
+
let verbose = options.verbose;
|
|
31
|
+
let debug = options.debug;
|
|
32
|
+
let projectRoot = options.projectRoot || process.cwd();
|
|
33
|
+
|
|
34
|
+
// Fallback to process.argv if called without options
|
|
35
|
+
if (!options.configPath && !options.verbose && !options.debug) {
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
configPath = args.includes('--config') ? args[args.indexOf('--config') + 1] : null;
|
|
38
|
+
verbose = args.includes('--verbose');
|
|
39
|
+
debug = args.includes('--debug');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!verbose) {
|
|
43
|
+
console.log('Building SEA...');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = loadConfig(configPath, projectRoot);
|
|
47
|
+
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.log('\n[1/8] Loading configuration');
|
|
50
|
+
console.log('Config:', JSON.stringify(config, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = config.targets[0];
|
|
54
|
+
const { nodeVersion, platform, arch } = parseTarget(target);
|
|
55
|
+
|
|
56
|
+
if (verbose) {
|
|
57
|
+
console.log(`Target: Node ${nodeVersion} on ${platform}-${arch}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.rebuild) {
|
|
61
|
+
if (verbose) console.log('\n[1b] Rebuilding native modules');
|
|
62
|
+
const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.js');
|
|
63
|
+
try {
|
|
64
|
+
execSync(`node "${rebuildScript}" --target ${target} "${projectRoot}"`, {
|
|
65
|
+
stdio: verbose ? 'inherit' : 'ignore'
|
|
66
|
+
});
|
|
67
|
+
if (verbose) console.log('✓ Native modules rebuilt');
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('✗ Native module rebuild failed:', error.message);
|
|
70
|
+
throw new Error('Native module rebuild failed. See output above for details.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const assets = await scanAssets(
|
|
75
|
+
config.assets,
|
|
76
|
+
config.binaries || [],
|
|
77
|
+
config.exclude || [],
|
|
78
|
+
projectRoot
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const { binaries, nonBinaries } = groupAssets(assets);
|
|
82
|
+
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(`\n[2/8] Scanning assets`);
|
|
85
|
+
console.log(`Found ${assets.length} assets (${binaries.length} binaries, ${nonBinaries.length} non-binaries)`);
|
|
86
|
+
console.log('Binaries:', binaries.map(b => b.assetKey));
|
|
87
|
+
const nonBinariesList = assets.filter(a => !a.isBinary);
|
|
88
|
+
console.log('Non-binary assets:', nonBinariesList.map(a => a.assetKey));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let encryptionKey = null;
|
|
92
|
+
let encryptedAssetKeys = new Set();
|
|
93
|
+
|
|
94
|
+
if (config.encryptAssets) {
|
|
95
|
+
if (verbose) console.log('\n[2b] Encrypting assets');
|
|
96
|
+
encryptionKey = generateEncryptionKey();
|
|
97
|
+
|
|
98
|
+
const excludeFromEncryption = [
|
|
99
|
+
'sea-manifest.json',
|
|
100
|
+
...(config.encryptExclude || [])
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const encryptedMap = encryptAssets(assets, encryptionKey, excludeFromEncryption);
|
|
104
|
+
|
|
105
|
+
for (const asset of assets) {
|
|
106
|
+
if (encryptedMap.has(asset.assetKey)) {
|
|
107
|
+
asset.content = encryptedMap.get(asset.assetKey);
|
|
108
|
+
asset.encrypted = true;
|
|
109
|
+
encryptedAssetKeys.add(asset.assetKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (verbose) {
|
|
114
|
+
console.log(`Encrypted ${encryptedAssetKeys.size} assets`);
|
|
115
|
+
console.log('Encrypted assets:', Array.from(encryptedAssetKeys));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (verbose) console.log('\n[3/8] Generating manifest');
|
|
120
|
+
const manifest = generateManifest(assets, config, platform, arch);
|
|
121
|
+
const manifestJson = serializeManifest(manifest);
|
|
122
|
+
|
|
123
|
+
const manifestAsset = {
|
|
124
|
+
sourcePath: null,
|
|
125
|
+
assetKey: 'sea-manifest.json',
|
|
126
|
+
isBinary: false,
|
|
127
|
+
content: Buffer.from(manifestJson, 'utf8')
|
|
128
|
+
};
|
|
129
|
+
assets.push(manifestAsset);
|
|
130
|
+
|
|
131
|
+
if (verbose) console.log('\n[4/8] Preparing bootstrap');
|
|
132
|
+
const bootstrapPath = path.join(__dirname, 'bootstrap.js');
|
|
133
|
+
const bootstrapContent = fs.readFileSync(bootstrapPath, 'utf8');
|
|
134
|
+
|
|
135
|
+
let augmentedBootstrap = bootstrapContent;
|
|
136
|
+
if (config.encryptAssets && encryptionKey && encryptedAssetKeys.size > 0) {
|
|
137
|
+
const keyCode = keyToObfuscatedCode(encryptionKey);
|
|
138
|
+
const encryptedKeysJson = JSON.stringify(Array.from(encryptedAssetKeys));
|
|
139
|
+
|
|
140
|
+
const encryptionSetup = `
|
|
141
|
+
const SEA_ENCRYPTION_KEY = ${keyCode};
|
|
142
|
+
const SEA_ENCRYPTED_ASSETS = new Set(${encryptedKeysJson});
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
augmentedBootstrap = bootstrapContent.replace(
|
|
146
|
+
" 'use strict';",
|
|
147
|
+
" 'use strict';\n" + encryptionSetup
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (verbose) console.log('Injecting encryption key and obfuscating bootstrap');
|
|
151
|
+
augmentedBootstrap = obfuscateBootstrap(augmentedBootstrap);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const entryPath = path.resolve(projectRoot, config.entry);
|
|
155
|
+
let entryContent = fs.readFileSync(entryPath, 'utf8');
|
|
156
|
+
|
|
157
|
+
// Strip shebang if present (e.g., #!/usr/bin/env node) - handle both Unix and Windows line endings
|
|
158
|
+
entryContent = entryContent.replace(/^#!.*(\r?\n)/, '');
|
|
159
|
+
|
|
160
|
+
// When using snapshot, wrap the application in v8.startupSnapshot.setDeserializeMainFunction
|
|
161
|
+
let bundledEntry;
|
|
162
|
+
if (config.useSnapshot) {
|
|
163
|
+
// Snapshot mode: Bootstrap runs at build time to set up deserialization callback
|
|
164
|
+
// Application code runs at runtime inside the callback
|
|
165
|
+
|
|
166
|
+
bundledEntry = `${augmentedBootstrap}\n\n`;
|
|
167
|
+
bundledEntry += `(function() {\n`;
|
|
168
|
+
bundledEntry += ` const v8 = require('v8');\n`;
|
|
169
|
+
bundledEntry += ` if (v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot()) {\n`;
|
|
170
|
+
bundledEntry += ` v8.startupSnapshot.setDeserializeMainFunction(() => {\n`;
|
|
171
|
+
bundledEntry += entryContent + '\n';
|
|
172
|
+
bundledEntry += ` });\n`;
|
|
173
|
+
bundledEntry += ` } else {\n`;
|
|
174
|
+
bundledEntry += entryContent + '\n';
|
|
175
|
+
bundledEntry += ` }\n`;
|
|
176
|
+
bundledEntry += `})();\n`;
|
|
177
|
+
} else {
|
|
178
|
+
const requireOverride = `
|
|
179
|
+
(function() {
|
|
180
|
+
const originalRequire = require;
|
|
181
|
+
const __seaNativeModuleMap = typeof global.__seaNativeModuleMap !== 'undefined' ? global.__seaNativeModuleMap : {};
|
|
182
|
+
const __seaCacheDir = typeof global.__seaCacheDir !== 'undefined' ? global.__seaCacheDir : '';
|
|
183
|
+
|
|
184
|
+
require = function(id) {
|
|
185
|
+
if (typeof id === 'string' && id.endsWith('.node')) {
|
|
186
|
+
const path = originalRequire('path');
|
|
187
|
+
const basename = path.basename(id);
|
|
188
|
+
|
|
189
|
+
if (__seaNativeModuleMap[basename]) {
|
|
190
|
+
const exports = {};
|
|
191
|
+
process.dlopen({ exports }, __seaNativeModuleMap[basename]);
|
|
192
|
+
return exports;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (path.isAbsolute(id) && id.startsWith(__seaCacheDir)) {
|
|
196
|
+
const exports = {};
|
|
197
|
+
process.dlopen({ exports }, id);
|
|
198
|
+
return exports;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (id === 'bindings') {
|
|
203
|
+
return function(name) {
|
|
204
|
+
if (!name.endsWith('.node')) {
|
|
205
|
+
name += '.node';
|
|
206
|
+
}
|
|
207
|
+
if (__seaNativeModuleMap[name]) {
|
|
208
|
+
const exports = {};
|
|
209
|
+
process.dlopen({ exports }, __seaNativeModuleMap[name]);
|
|
210
|
+
return exports;
|
|
211
|
+
}
|
|
212
|
+
throw new Error('Could not load native module "' + name + '" - not found in SEA cache');
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return originalRequire.call(this, id);
|
|
217
|
+
};
|
|
218
|
+
})();
|
|
219
|
+
`;
|
|
220
|
+
bundledEntry = `${augmentedBootstrap}\n\n${requireOverride}\n${entryContent}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bundledEntryPath = path.join(projectRoot, 'out', '_sea-entry.js');
|
|
224
|
+
|
|
225
|
+
fs.mkdirSync(path.dirname(bundledEntryPath), { recursive: true });
|
|
226
|
+
fs.writeFileSync(bundledEntryPath, bundledEntry, 'utf8');
|
|
227
|
+
|
|
228
|
+
if (verbose) console.log(`Bundled entry: ${bundledEntryPath}`);
|
|
229
|
+
|
|
230
|
+
if (verbose) console.log('\n[5/8] Creating SEA blob configuration');
|
|
231
|
+
const blobOutputPath = path.join(projectRoot, config.outputPath, 'sea-blob.bin');
|
|
232
|
+
const seaConfig = createSeaConfig(bundledEntryPath, blobOutputPath, assets, config);
|
|
233
|
+
|
|
234
|
+
const seaConfigPath = path.join(projectRoot, config.outputPath, 'sea-config.json');
|
|
235
|
+
const tempDir = path.join(projectRoot, config.outputPath, '.sea-temp');
|
|
236
|
+
fs.mkdirSync(path.dirname(seaConfigPath), { recursive: true });
|
|
237
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
238
|
+
writeSeaConfigJson(seaConfig, seaConfigPath, assets, tempDir);
|
|
239
|
+
|
|
240
|
+
if (verbose) {
|
|
241
|
+
console.log(`SEA config: ${seaConfigPath}`);
|
|
242
|
+
console.log('\n[6/8] Generating SEA blob');
|
|
243
|
+
}
|
|
244
|
+
await generateBlob(seaConfigPath);
|
|
245
|
+
|
|
246
|
+
if (verbose) console.log('\n[7/8] Fetching Node.js binary');
|
|
247
|
+
const cacheDir = path.join(projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
|
|
248
|
+
const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
|
|
249
|
+
|
|
250
|
+
if (verbose) console.log('\n[8/8] Injecting SEA blob into Node binary');
|
|
251
|
+
const outputExe = path.join(projectRoot, config.outputPath, config.output);
|
|
252
|
+
await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, verbose);
|
|
253
|
+
|
|
254
|
+
if (!debug) {
|
|
255
|
+
if (verbose) console.log('\nCleaning up temporary files');
|
|
256
|
+
const filesToClean = [seaConfigPath, blobOutputPath];
|
|
257
|
+
for (const file of filesToClean) {
|
|
258
|
+
if (fs.existsSync(file)) {
|
|
259
|
+
fs.unlinkSync(file);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (fs.existsSync(tempDir)) {
|
|
263
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const sizeMB = (fs.statSync(outputExe).size / 1024 / 1024).toFixed(2);
|
|
268
|
+
console.log(`\n✓ Build complete: ${config.output} (${sizeMB} MB)`);
|
|
269
|
+
if (verbose) console.log(`Output path: ${outputExe}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run if called directly
|
|
273
|
+
if (require.main === module) {
|
|
274
|
+
build().catch(error => {
|
|
275
|
+
console.error('Build failed:', error.message);
|
|
276
|
+
if (process.argv.includes('--verbose')) {
|
|
277
|
+
console.error(error.stack);
|
|
278
|
+
}
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { build };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.js
|
|
3
|
+
* Load and validate SEA configuration from package.json or standalone config file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SeaConfig
|
|
11
|
+
* @property {string} entry - Path to the bundled application entry script
|
|
12
|
+
* @property {string[]} assets - Array of glob patterns or paths to include in SEA blob (supports '!' prefix for exclusions)
|
|
13
|
+
* @property {string[]} targets - Array of target specifiers (e.g., "node24.11.0-win-x64")
|
|
14
|
+
* @property {string} output - Output executable filename
|
|
15
|
+
* @property {string} outputPath - Directory for the final executable
|
|
16
|
+
* @property {string[]} [binaries] - Array of binary filename patterns to extract at runtime
|
|
17
|
+
* @property {boolean} [disableExperimentalSEAWarning] - Suppress SEA experimental warning
|
|
18
|
+
* @property {boolean} [useSnapshot] - Enable V8 snapshot
|
|
19
|
+
* @property {boolean} [useCodeCache] - Enable V8 code cache
|
|
20
|
+
* @property {string[]} [exclude] - (Legacy) Glob patterns to exclude from assets - use '!' prefix in assets instead
|
|
21
|
+
* @property {boolean} [encryptAssets] - Enable asset encryption (default: false)
|
|
22
|
+
* @property {string[]} [encryptExclude] - Asset patterns to exclude from encryption
|
|
23
|
+
* @property {boolean} [rebuild] - Automatically rebuild native modules for target platform (default: false)
|
|
24
|
+
* @property {boolean} [verbose] - Enable diagnostic logging
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load SEA configuration from package.json or a standalone file.
|
|
29
|
+
* @param {string} [configPath] - Optional path to a standalone config file
|
|
30
|
+
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
31
|
+
* @returns {SeaConfig}
|
|
32
|
+
*/
|
|
33
|
+
function loadConfig(configPath, projectRoot = process.cwd()) {
|
|
34
|
+
let config;
|
|
35
|
+
|
|
36
|
+
if (configPath) {
|
|
37
|
+
// Load from standalone config file
|
|
38
|
+
const fullPath = path.resolve(projectRoot, configPath);
|
|
39
|
+
if (!fs.existsSync(fullPath)) {
|
|
40
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
41
|
+
}
|
|
42
|
+
config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
43
|
+
} else {
|
|
44
|
+
// Load from package.json
|
|
45
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
46
|
+
if (!fs.existsSync(pkgPath)) {
|
|
47
|
+
throw new Error(`package.json not found in: ${projectRoot}`);
|
|
48
|
+
}
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
50
|
+
|
|
51
|
+
// Check for 'sea' field first, fall back to 'pkg' for migration compatibility
|
|
52
|
+
config = pkg.seabox || pkg.pkg;
|
|
53
|
+
|
|
54
|
+
if (!config) {
|
|
55
|
+
throw new Error('No "seabox" or "pkg" configuration found in package.json');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Attach package metadata for manifest
|
|
59
|
+
config._packageName = pkg.name;
|
|
60
|
+
config._packageVersion = pkg.version;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validateConfig(config);
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate required configuration fields.
|
|
69
|
+
* @param {SeaConfig} config
|
|
70
|
+
*/
|
|
71
|
+
function validateConfig(config) {
|
|
72
|
+
const required = ['entry', 'assets', 'targets', 'output', 'outputPath'];
|
|
73
|
+
const missing = required.filter(field => !config[field]);
|
|
74
|
+
|
|
75
|
+
if (missing.length > 0) {
|
|
76
|
+
throw new Error(`Missing required SEA config fields: ${missing.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!Array.isArray(config.assets) || config.assets.length === 0) {
|
|
80
|
+
throw new Error('SEA config "assets" must be a non-empty array');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(config.targets) || config.targets.length === 0) {
|
|
84
|
+
throw new Error('SEA config "targets" must be a non-empty array');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate target format (e.g., "node24.11.0-win-x64")
|
|
88
|
+
const targetPattern = /^node\d+\.\d+\.\d+-\w+-\w+$/;
|
|
89
|
+
config.targets.forEach(target => {
|
|
90
|
+
if (!targetPattern.test(target)) {
|
|
91
|
+
throw new Error(`Invalid target format: "${target}". Expected format: "nodeX.Y.Z-platform-arch"`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse a target string into components.
|
|
98
|
+
* @param {string} target - e.g., "node24.11.0-win-x64"
|
|
99
|
+
* @returns {{nodeVersion: string, platform: string, arch: string}}
|
|
100
|
+
*/
|
|
101
|
+
function parseTarget(target) {
|
|
102
|
+
const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
|
|
103
|
+
if (!match) {
|
|
104
|
+
throw new Error(`Cannot parse target: ${target}`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
nodeVersion: match[1],
|
|
108
|
+
platform: match[2],
|
|
109
|
+
arch: match[3]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
loadConfig,
|
|
115
|
+
validateConfig,
|
|
116
|
+
parseTarget
|
|
117
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crypto-assets.js
|
|
3
|
+
* Asset encryption/decryption utilities for SEA applications.
|
|
4
|
+
* The encryption key is embedded in the bootstrap code and, when using snapshots,
|
|
5
|
+
* becomes part of the V8 bytecode, providing obfuscation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a random encryption key.
|
|
12
|
+
* @returns {Buffer} - 32-byte key for AES-256
|
|
13
|
+
*/
|
|
14
|
+
function generateEncryptionKey() {
|
|
15
|
+
return crypto.randomBytes(32);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encrypt asset data using AES-256-GCM.
|
|
20
|
+
* @param {Buffer} data - Asset data to encrypt
|
|
21
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
22
|
+
* @returns {Buffer} - Encrypted data with IV and auth tag prepended
|
|
23
|
+
*/
|
|
24
|
+
function encryptAsset(data, key) {
|
|
25
|
+
// Generate a random IV for this encryption
|
|
26
|
+
const iv = crypto.randomBytes(16);
|
|
27
|
+
|
|
28
|
+
// Create cipher
|
|
29
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
30
|
+
|
|
31
|
+
// Encrypt the data
|
|
32
|
+
const encrypted = Buffer.concat([
|
|
33
|
+
cipher.update(data),
|
|
34
|
+
cipher.final()
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Get the authentication tag
|
|
38
|
+
const authTag = cipher.getAuthTag();
|
|
39
|
+
|
|
40
|
+
// Format: [IV (16 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
|
|
41
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decrypt asset data using AES-256-GCM.
|
|
46
|
+
* @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
|
|
47
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
48
|
+
* @returns {Buffer} - Decrypted data
|
|
49
|
+
*/
|
|
50
|
+
function decryptAsset(encryptedData, key) {
|
|
51
|
+
// Extract IV, auth tag, and encrypted content
|
|
52
|
+
const iv = encryptedData.slice(0, 16);
|
|
53
|
+
const authTag = encryptedData.slice(16, 32);
|
|
54
|
+
const encrypted = encryptedData.slice(32);
|
|
55
|
+
|
|
56
|
+
// Create decipher
|
|
57
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
58
|
+
decipher.setAuthTag(authTag);
|
|
59
|
+
|
|
60
|
+
// Decrypt the data
|
|
61
|
+
const decrypted = Buffer.concat([
|
|
62
|
+
decipher.update(encrypted),
|
|
63
|
+
decipher.final()
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return decrypted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encrypt multiple assets.
|
|
71
|
+
* @param {import('./scanner').AssetEntry[]} assets - Assets to encrypt
|
|
72
|
+
* @param {Buffer} key - Encryption key
|
|
73
|
+
* @param {string[]} [excludePatterns] - Patterns to exclude from encryption (e.g., ['*.txt'])
|
|
74
|
+
* @returns {Map<string, Buffer>} - Map of asset key to encrypted content
|
|
75
|
+
*/
|
|
76
|
+
function encryptAssets(assets, key, excludePatterns = []) {
|
|
77
|
+
const encrypted = new Map();
|
|
78
|
+
|
|
79
|
+
for (const asset of assets) {
|
|
80
|
+
// Skip binaries - they need to be extracted as-is
|
|
81
|
+
if (asset.isBinary) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if this asset should be excluded from encryption
|
|
86
|
+
let shouldExclude = false;
|
|
87
|
+
for (const pattern of excludePatterns) {
|
|
88
|
+
if (asset.assetKey.includes(pattern) || asset.assetKey.endsWith(pattern)) {
|
|
89
|
+
shouldExclude = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (shouldExclude) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get asset content
|
|
99
|
+
const content = asset.content || require('fs').readFileSync(asset.sourcePath);
|
|
100
|
+
|
|
101
|
+
// Encrypt it
|
|
102
|
+
const encryptedContent = encryptAsset(content, key);
|
|
103
|
+
encrypted.set(asset.assetKey, encryptedContent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return encrypted;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate the encryption key as a code string for embedding in bootstrap.
|
|
111
|
+
* Returns a string that evaluates to a Buffer containing the key.
|
|
112
|
+
* @param {Buffer} key - Encryption key
|
|
113
|
+
* @returns {string} - JavaScript code that creates the key Buffer
|
|
114
|
+
*/
|
|
115
|
+
function keyToCode(key) {
|
|
116
|
+
// Convert key to hex string for embedding
|
|
117
|
+
const hexKey = key.toString('hex');
|
|
118
|
+
|
|
119
|
+
// Return code that reconstructs the Buffer
|
|
120
|
+
// Using multiple obfuscation techniques:
|
|
121
|
+
// 1. Split the hex string
|
|
122
|
+
// 2. Use Array.from with character codes
|
|
123
|
+
// 3. XOR with a simple constant (adds one more layer)
|
|
124
|
+
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (let i = 0; i < hexKey.length; i += 8) {
|
|
127
|
+
parts.push(hexKey.slice(i, i + 8));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return `Buffer.from('${hexKey}', 'hex')`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a more obfuscated version of the key embedding code.
|
|
135
|
+
* This version splits the key and uses character code manipulation.
|
|
136
|
+
* @param {Buffer} key - Encryption key
|
|
137
|
+
* @returns {string} - Obfuscated JavaScript code
|
|
138
|
+
*/
|
|
139
|
+
function keyToObfuscatedCode(key) {
|
|
140
|
+
// Convert to array of byte values
|
|
141
|
+
const bytes = Array.from(key);
|
|
142
|
+
|
|
143
|
+
// Create a simple XOR mask
|
|
144
|
+
const xorMask = 0x5A;
|
|
145
|
+
|
|
146
|
+
// XOR each byte with the mask
|
|
147
|
+
const masked = bytes.map(b => b ^ xorMask);
|
|
148
|
+
|
|
149
|
+
// Return code that reconstructs the key
|
|
150
|
+
return `Buffer.from([${masked.join(',')}].map(b => b ^ 0x${xorMask.toString(16)}))`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
generateEncryptionKey,
|
|
155
|
+
encryptAsset,
|
|
156
|
+
decryptAsset,
|
|
157
|
+
encryptAssets,
|
|
158
|
+
keyToCode,
|
|
159
|
+
keyToObfuscatedCode
|
|
160
|
+
};
|