@ecmaos/coreutils 0.6.2 → 0.6.4
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/commands/crypto.d.ts +4 -0
- package/dist/commands/crypto.d.ts.map +1 -0
- package/dist/commands/crypto.js +1322 -0
- package/dist/commands/crypto.js.map +1 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +9 -8
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/view.js +1 -1
- package/dist/commands/view.js.map +1 -1
- package/dist/index.d.ts +34 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +119 -116
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/crypto.ts +1498 -0
- package/src/commands/ls.ts +11 -5
- package/src/commands/view.ts +1 -1
- package/src/index.ts +121 -118
|
@@ -0,0 +1,1322 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { TerminalCommand } from '../shared/terminal-command.js';
|
|
4
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js';
|
|
5
|
+
const SUPPORTED_SYMMETRIC_ALGORITHMS = {
|
|
6
|
+
'aes-gcm': 'AES-GCM',
|
|
7
|
+
'aes-cbc': 'AES-CBC',
|
|
8
|
+
'aes-ctr': 'AES-CTR',
|
|
9
|
+
'aes-kw': 'AES-KW'
|
|
10
|
+
};
|
|
11
|
+
const SUPPORTED_ASYMMETRIC_ALGORITHMS = {
|
|
12
|
+
'rsa-oaep': 'RSA-OAEP',
|
|
13
|
+
'rsa-pss': 'RSA-PSS',
|
|
14
|
+
'rsassa-pkcs1-v1_5': 'RSASSA-PKCS1-v1_5',
|
|
15
|
+
'rsassa-pkcs1-v1-5': 'RSASSA-PKCS1-v1_5',
|
|
16
|
+
'ecdsa': 'ECDSA',
|
|
17
|
+
'ecdh': 'ECDH'
|
|
18
|
+
};
|
|
19
|
+
const SUPPORTED_SIGN_ALGORITHMS = {
|
|
20
|
+
'ecdsa': 'ECDSA',
|
|
21
|
+
'rsa-pss': 'RSA-PSS',
|
|
22
|
+
'rsassa-pkcs1-v1_5': 'RSASSA-PKCS1-v1_5',
|
|
23
|
+
'rsassa-pkcs1-v1-5': 'RSASSA-PKCS1-v1_5',
|
|
24
|
+
'hmac': 'HMAC'
|
|
25
|
+
};
|
|
26
|
+
const SUPPORTED_DERIVE_ALGORITHMS = {
|
|
27
|
+
'pbkdf2': 'PBKDF2',
|
|
28
|
+
'hkdf': 'HKDF',
|
|
29
|
+
'ecdh': 'ECDH'
|
|
30
|
+
};
|
|
31
|
+
const SUPPORTED_HASH_ALGORITHMS = {
|
|
32
|
+
'sha1': 'SHA-1',
|
|
33
|
+
'sha-1': 'SHA-1',
|
|
34
|
+
'sha256': 'SHA-256',
|
|
35
|
+
'sha-256': 'SHA-256',
|
|
36
|
+
'sha384': 'SHA-384',
|
|
37
|
+
'sha-384': 'SHA-384',
|
|
38
|
+
'sha512': 'SHA-512',
|
|
39
|
+
'sha-512': 'SHA-512'
|
|
40
|
+
};
|
|
41
|
+
const SUPPORTED_NAMED_CURVES = {
|
|
42
|
+
'p-256': 'P-256',
|
|
43
|
+
'p256': 'P-256',
|
|
44
|
+
'p-384': 'P-384',
|
|
45
|
+
'p384': 'P-384',
|
|
46
|
+
'p-521': 'P-521',
|
|
47
|
+
'p521': 'P-521'
|
|
48
|
+
};
|
|
49
|
+
const SUPPORTED_KEY_FORMATS = {
|
|
50
|
+
'jwk': 'jwk',
|
|
51
|
+
'raw': 'raw',
|
|
52
|
+
'pkcs8': 'pkcs8',
|
|
53
|
+
'spki': 'spki'
|
|
54
|
+
};
|
|
55
|
+
function printUsage(process, terminal) {
|
|
56
|
+
const usage = `Usage: crypto <subcommand> [options]
|
|
57
|
+
|
|
58
|
+
Subcommands:
|
|
59
|
+
generate Generate cryptographic keys
|
|
60
|
+
encrypt Encrypt data
|
|
61
|
+
decrypt Decrypt data
|
|
62
|
+
sign Sign data
|
|
63
|
+
verify Verify signatures
|
|
64
|
+
import Import keys from various formats
|
|
65
|
+
export Export keys to various formats
|
|
66
|
+
derive Derive keys from passwords or other keys
|
|
67
|
+
random Generate random bytes
|
|
68
|
+
|
|
69
|
+
--help display this help and exit
|
|
70
|
+
|
|
71
|
+
Run 'crypto <subcommand> --help' for subcommand-specific help.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
|
|
75
|
+
# Symmetric encryption
|
|
76
|
+
crypto generate --algorithm aes-gcm --length 256 --output key.json
|
|
77
|
+
crypto encrypt --algorithm aes-gcm --key-file key.json --input plaintext.txt --output encrypted.bin
|
|
78
|
+
crypto decrypt --algorithm aes-gcm --key-file key.json --input encrypted.bin --output decrypted.txt
|
|
79
|
+
|
|
80
|
+
# ECDSA signing and verification
|
|
81
|
+
crypto generate --algorithm ecdsa --named-curve P-256 --output ecdsa-key.json
|
|
82
|
+
crypto sign --algorithm ecdsa --key-file ecdsa-key.json --input message.txt --output signature.sig
|
|
83
|
+
crypto verify --algorithm ecdsa --key-file ecdsa-key.json --input message.txt --signature signature.sig
|
|
84
|
+
|
|
85
|
+
# Key format conversion
|
|
86
|
+
crypto import --format jwk --input key.json --output key.pem
|
|
87
|
+
crypto export --format pkcs8 --input key.pem --output key.json`;
|
|
88
|
+
void writelnStderr(process, terminal, usage);
|
|
89
|
+
}
|
|
90
|
+
function toArrayBuffer(buffer) {
|
|
91
|
+
if (buffer instanceof ArrayBuffer) {
|
|
92
|
+
return buffer;
|
|
93
|
+
}
|
|
94
|
+
if (buffer instanceof SharedArrayBuffer) {
|
|
95
|
+
const view = new Uint8Array(buffer);
|
|
96
|
+
const newBuffer = new ArrayBuffer(view.length);
|
|
97
|
+
new Uint8Array(newBuffer).set(view);
|
|
98
|
+
return newBuffer;
|
|
99
|
+
}
|
|
100
|
+
const view = new Uint8Array(buffer);
|
|
101
|
+
const newBuffer = new ArrayBuffer(view.length);
|
|
102
|
+
new Uint8Array(newBuffer).set(view);
|
|
103
|
+
return newBuffer;
|
|
104
|
+
}
|
|
105
|
+
async function readStreamToUint8Array(reader) {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
try {
|
|
108
|
+
while (true) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done)
|
|
111
|
+
break;
|
|
112
|
+
if (value) {
|
|
113
|
+
chunks.push(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
reader.releaseLock();
|
|
119
|
+
}
|
|
120
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
121
|
+
const result = new Uint8Array(totalLength);
|
|
122
|
+
let offset = 0;
|
|
123
|
+
for (const chunk of chunks) {
|
|
124
|
+
result.set(chunk, offset);
|
|
125
|
+
offset += chunk.length;
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
async function readFileToUint8Array(fs, filePath) {
|
|
130
|
+
const handle = await fs.open(filePath, 'r');
|
|
131
|
+
const stat = await fs.stat(filePath);
|
|
132
|
+
const chunks = [];
|
|
133
|
+
let bytesRead = 0;
|
|
134
|
+
const chunkSize = 64 * 1024;
|
|
135
|
+
while (bytesRead < stat.size) {
|
|
136
|
+
const data = new Uint8Array(chunkSize);
|
|
137
|
+
const readSize = Math.min(chunkSize, stat.size - bytesRead);
|
|
138
|
+
await handle.read(data, 0, readSize, bytesRead);
|
|
139
|
+
chunks.push(data.subarray(0, readSize));
|
|
140
|
+
bytesRead += readSize;
|
|
141
|
+
}
|
|
142
|
+
await handle.close();
|
|
143
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
144
|
+
const fileData = new Uint8Array(totalLength);
|
|
145
|
+
let offset = 0;
|
|
146
|
+
for (const chunk of chunks) {
|
|
147
|
+
fileData.set(chunk, offset);
|
|
148
|
+
offset += chunk.length;
|
|
149
|
+
}
|
|
150
|
+
return fileData;
|
|
151
|
+
}
|
|
152
|
+
async function writeUint8ArrayToFile(fs, filePath, data) {
|
|
153
|
+
const handle = await fs.open(filePath, 'w');
|
|
154
|
+
await handle.write(data);
|
|
155
|
+
await handle.close();
|
|
156
|
+
}
|
|
157
|
+
function parseArgs(args) {
|
|
158
|
+
const parsed = {};
|
|
159
|
+
let i = 0;
|
|
160
|
+
while (i < args.length) {
|
|
161
|
+
const arg = args[i];
|
|
162
|
+
if (!arg) {
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg === '--help' || arg === '-h') {
|
|
167
|
+
parsed.help = true;
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
else if (arg.startsWith('--')) {
|
|
171
|
+
const key = arg.slice(2);
|
|
172
|
+
if (arg.includes('=')) {
|
|
173
|
+
const parts = arg.split('=', 2);
|
|
174
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
175
|
+
parsed[parts[0].slice(2)] = parts[1];
|
|
176
|
+
}
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
else if (i + 1 < args.length && !args[i + 1]?.startsWith('--')) {
|
|
180
|
+
const nextArg = args[i + 1];
|
|
181
|
+
if (nextArg) {
|
|
182
|
+
parsed[key] = nextArg;
|
|
183
|
+
}
|
|
184
|
+
i += 2;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
parsed[key] = true;
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (arg.startsWith('-') && arg.length === 2) {
|
|
192
|
+
const key = arg.slice(1);
|
|
193
|
+
if (i + 1 < args.length && !args[i + 1]?.startsWith('-')) {
|
|
194
|
+
const nextArg = args[i + 1];
|
|
195
|
+
if (nextArg) {
|
|
196
|
+
parsed[key] = nextArg;
|
|
197
|
+
}
|
|
198
|
+
i += 2;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
parsed[key] = true;
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (!parsed._)
|
|
207
|
+
parsed._ = [];
|
|
208
|
+
if (typeof parsed._ === 'string')
|
|
209
|
+
parsed._ = [parsed._];
|
|
210
|
+
if (Array.isArray(parsed._)) {
|
|
211
|
+
parsed._.push(arg);
|
|
212
|
+
}
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return parsed;
|
|
217
|
+
}
|
|
218
|
+
async function handleGenerate(shell, terminal, process, args) {
|
|
219
|
+
const usage = `Usage: crypto generate [OPTIONS]
|
|
220
|
+
|
|
221
|
+
Generate cryptographic keys.
|
|
222
|
+
|
|
223
|
+
Options:
|
|
224
|
+
--algorithm, -a ALGORITHM Algorithm (AES-GCM, AES-CBC, AES-CTR, AES-KW, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDSA, ECDH, HMAC)
|
|
225
|
+
--length, -l LENGTH Key length in bits (for AES: 128, 192, 256; for RSA: 1024, 2048, 4096)
|
|
226
|
+
--named-curve, -c CURVE Named curve for ECDSA/ECDH (P-256, P-384, P-521)
|
|
227
|
+
--hash, -h ALGORITHM Hash algorithm for HMAC/RSA (SHA-1, SHA-256, SHA-384, SHA-512)
|
|
228
|
+
--output, -o FILE Output file (default: stdout, JWK format)
|
|
229
|
+
--format, -f FORMAT Output format (jwk, raw, pkcs8, spki) (default: jwk)
|
|
230
|
+
--help Display this help
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
crypto generate --algorithm aes-gcm --length 256 --output key.json
|
|
234
|
+
crypto generate --algorithm ecdsa --named-curve P-256 --output ecdsa-key.json
|
|
235
|
+
crypto generate --algorithm rsa-oaep --length 2048 --output rsa-key.json
|
|
236
|
+
crypto generate --algorithm hmac --hash SHA-256 --length 256 --output hmac-key.json`;
|
|
237
|
+
const parsed = parseArgs(args);
|
|
238
|
+
if (parsed.help) {
|
|
239
|
+
await writelnStderr(process, terminal, usage);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
243
|
+
const length = parsed.length || parsed.l;
|
|
244
|
+
const namedCurve = parsed['named-curve'] || parsed.c;
|
|
245
|
+
const hash = parsed.hash || parsed.h;
|
|
246
|
+
const output = parsed.output || parsed.o;
|
|
247
|
+
const formatValue = parsed.format || parsed.f || 'jwk';
|
|
248
|
+
const format = (typeof formatValue === 'string' ? formatValue : 'jwk').toLowerCase();
|
|
249
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
250
|
+
await writelnStderr(process, terminal, 'crypto generate: --algorithm is required');
|
|
251
|
+
await writelnStderr(process, terminal, 'Try "crypto generate --help" for more information.');
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
const algoLower = algorithm.toLowerCase();
|
|
255
|
+
const keyFormat = SUPPORTED_KEY_FORMATS[format];
|
|
256
|
+
if (!keyFormat) {
|
|
257
|
+
await writelnStderr(process, terminal, `crypto generate: unsupported format '${format}'`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
let key;
|
|
262
|
+
let exportFormat = keyFormat;
|
|
263
|
+
if (SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower]) {
|
|
264
|
+
const symAlgo = SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower];
|
|
265
|
+
const keyLength = length ? parseInt(length, 10) : 256;
|
|
266
|
+
if (![128, 192, 256].includes(keyLength)) {
|
|
267
|
+
await writelnStderr(process, terminal, 'crypto generate: AES key length must be 128, 192, or 256');
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
key = await crypto.subtle.generateKey({ name: symAlgo, length: keyLength }, true, ['encrypt', 'decrypt']);
|
|
271
|
+
if (keyFormat === 'pkcs8' || keyFormat === 'spki') {
|
|
272
|
+
await writelnStderr(process, terminal, 'crypto generate: symmetric keys cannot be exported in PKCS8 or SPKI format');
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
exportFormat = keyFormat === 'raw' ? 'raw' : 'jwk';
|
|
276
|
+
}
|
|
277
|
+
else if (SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDSA' || SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDH') {
|
|
278
|
+
const curve = namedCurve ? (SUPPORTED_NAMED_CURVES[namedCurve.toLowerCase()] || 'P-256') : 'P-256';
|
|
279
|
+
const keyUsages = SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDSA'
|
|
280
|
+
? ['sign', 'verify']
|
|
281
|
+
: ['deriveKey', 'deriveBits'];
|
|
282
|
+
key = await crypto.subtle.generateKey({
|
|
283
|
+
name: SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower],
|
|
284
|
+
namedCurve: curve
|
|
285
|
+
}, true, keyUsages);
|
|
286
|
+
if (keyFormat === 'raw') {
|
|
287
|
+
await writelnStderr(process, terminal, 'crypto generate: ECDSA/ECDH keys cannot be exported in raw format');
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower]?.startsWith('RSA')) {
|
|
292
|
+
const rsaAlgo = SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower];
|
|
293
|
+
const keyLength = length ? parseInt(length, 10) : 2048;
|
|
294
|
+
const hashAlgo = hash ? (SUPPORTED_HASH_ALGORITHMS[hash.toLowerCase()] || 'SHA-256') : 'SHA-256';
|
|
295
|
+
if (![1024, 2048, 4096].includes(keyLength)) {
|
|
296
|
+
await writelnStderr(process, terminal, 'crypto generate: RSA key length must be 1024, 2048, or 4096');
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const keyUsages = rsaAlgo === 'RSA-OAEP'
|
|
300
|
+
? ['encrypt', 'decrypt']
|
|
301
|
+
: ['sign', 'verify'];
|
|
302
|
+
key = await crypto.subtle.generateKey({
|
|
303
|
+
name: rsaAlgo,
|
|
304
|
+
modulusLength: keyLength,
|
|
305
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
306
|
+
hash: hashAlgo
|
|
307
|
+
}, true, keyUsages);
|
|
308
|
+
if (keyFormat === 'raw') {
|
|
309
|
+
await writelnStderr(process, terminal, 'crypto generate: RSA keys cannot be exported in raw format');
|
|
310
|
+
return 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (algoLower === 'hmac') {
|
|
314
|
+
const keyLength = length ? parseInt(length, 10) : 256;
|
|
315
|
+
const hashAlgo = hash ? (SUPPORTED_HASH_ALGORITHMS[hash.toLowerCase()] || 'SHA-256') : 'SHA-256';
|
|
316
|
+
key = await crypto.subtle.generateKey({
|
|
317
|
+
name: 'HMAC',
|
|
318
|
+
hash: hashAlgo,
|
|
319
|
+
length: keyLength
|
|
320
|
+
}, true, ['sign', 'verify']);
|
|
321
|
+
if (keyFormat === 'pkcs8' || keyFormat === 'spki') {
|
|
322
|
+
await writelnStderr(process, terminal, 'crypto generate: HMAC keys cannot be exported in PKCS8 or SPKI format');
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
exportFormat = keyFormat === 'raw' ? 'raw' : 'jwk';
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
await writelnStderr(process, terminal, `crypto generate: unsupported algorithm '${algorithm}'`);
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
let exported;
|
|
332
|
+
if ('publicKey' in key && 'privateKey' in key) {
|
|
333
|
+
const keyPair = key;
|
|
334
|
+
if (keyFormat === 'spki') {
|
|
335
|
+
exported = await crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
336
|
+
}
|
|
337
|
+
else if (keyFormat === 'pkcs8') {
|
|
338
|
+
exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
exported = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
exported = await crypto.subtle.exportKey(exportFormat, key);
|
|
346
|
+
}
|
|
347
|
+
let outputData;
|
|
348
|
+
if (exportFormat === 'jwk') {
|
|
349
|
+
const json = JSON.stringify(exported, null, 2);
|
|
350
|
+
outputData = new TextEncoder().encode(json);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
outputData = new Uint8Array(exported);
|
|
354
|
+
}
|
|
355
|
+
if (output) {
|
|
356
|
+
const outputPath = path.resolve(shell.cwd, output);
|
|
357
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, outputPath, outputData);
|
|
358
|
+
await writelnStdout(process, terminal, chalk.green(`Key generated and saved to ${output}`));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
if (process?.stdout) {
|
|
362
|
+
const writer = process.stdout.getWriter();
|
|
363
|
+
try {
|
|
364
|
+
await writer.write(outputData);
|
|
365
|
+
if (exportFormat === 'jwk') {
|
|
366
|
+
await writer.write(new TextEncoder().encode('\n'));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
writer.releaseLock();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
await writelnStderr(process, terminal, `crypto generate: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function handleEncrypt(shell, terminal, process, args) {
|
|
385
|
+
const usage = `Usage: crypto encrypt [OPTIONS]
|
|
386
|
+
|
|
387
|
+
Encrypt data using various algorithms.
|
|
388
|
+
|
|
389
|
+
Options:
|
|
390
|
+
--algorithm, -a ALGORITHM Algorithm (AES-GCM, AES-CBC, AES-CTR, RSA-OAEP)
|
|
391
|
+
--key-file, -k FILE Key file (JWK format)
|
|
392
|
+
--input, -i FILE Input file (default: stdin)
|
|
393
|
+
--output, -o FILE Output file (default: stdout)
|
|
394
|
+
--iv-file FILE IV/nonce file (for AES, auto-generated if not provided)
|
|
395
|
+
--help Display this help`;
|
|
396
|
+
const parsed = parseArgs(args);
|
|
397
|
+
if (parsed.help) {
|
|
398
|
+
await writelnStderr(process, terminal, usage);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
402
|
+
const keyFile = parsed['key-file'] || parsed.k;
|
|
403
|
+
const input = parsed.input || parsed.i;
|
|
404
|
+
const output = parsed.output || parsed.o;
|
|
405
|
+
const ivFile = parsed['iv-file'];
|
|
406
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
407
|
+
await writelnStderr(process, terminal, 'crypto encrypt: --algorithm is required');
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
411
|
+
await writelnStderr(process, terminal, 'crypto encrypt: --key-file is required');
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const algoLower = algorithm.toLowerCase();
|
|
416
|
+
if (!SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower] && algoLower !== 'rsa-oaep') {
|
|
417
|
+
await writelnStderr(process, terminal, `crypto encrypt: unsupported algorithm '${algorithm}'`);
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
420
|
+
const keyPath = path.resolve(shell.cwd, keyFile);
|
|
421
|
+
const keyData = await readFileToUint8Array(shell.context.fs.promises, keyPath);
|
|
422
|
+
const keyJson = JSON.parse(new TextDecoder().decode(keyData));
|
|
423
|
+
let key;
|
|
424
|
+
let encryptParams;
|
|
425
|
+
if (SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower]) {
|
|
426
|
+
const symAlgo = SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower];
|
|
427
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: symAlgo }, false, ['encrypt']);
|
|
428
|
+
let iv;
|
|
429
|
+
if (ivFile && typeof ivFile === 'string') {
|
|
430
|
+
iv = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, ivFile));
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
if (symAlgo === 'AES-GCM') {
|
|
434
|
+
iv = crypto.getRandomValues(new Uint8Array(12));
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
iv = crypto.getRandomValues(new Uint8Array(16));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const ivBuffer = toArrayBuffer(iv.buffer);
|
|
441
|
+
if (symAlgo === 'AES-GCM') {
|
|
442
|
+
encryptParams = { name: 'AES-GCM', iv: new Uint8Array(ivBuffer, iv.byteOffset, iv.length) };
|
|
443
|
+
}
|
|
444
|
+
else if (symAlgo === 'AES-CBC') {
|
|
445
|
+
encryptParams = { name: 'AES-CBC', iv: new Uint8Array(ivBuffer, iv.byteOffset, iv.length) };
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
encryptParams = { name: 'AES-CTR', counter: new Uint8Array(ivBuffer, iv.byteOffset, iv.length), length: 128 };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
|
453
|
+
encryptParams = { name: 'RSA-OAEP' };
|
|
454
|
+
}
|
|
455
|
+
let inputData;
|
|
456
|
+
if (input) {
|
|
457
|
+
inputData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, input));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
if (!process?.stdin) {
|
|
461
|
+
await writelnStderr(process, terminal, 'crypto encrypt: no input specified');
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
const reader = process.stdin.getReader();
|
|
465
|
+
inputData = await readStreamToUint8Array(reader);
|
|
466
|
+
}
|
|
467
|
+
const inputBuffer = toArrayBuffer(inputData.buffer);
|
|
468
|
+
const encrypted = await crypto.subtle.encrypt(encryptParams, key, new Uint8Array(inputBuffer, inputData.byteOffset, inputData.length));
|
|
469
|
+
let outputData;
|
|
470
|
+
const encryptedArray = new Uint8Array(encrypted);
|
|
471
|
+
if (SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower] && !ivFile) {
|
|
472
|
+
const ivParam = encryptParams.iv;
|
|
473
|
+
const ivArray = ivParam instanceof Uint8Array
|
|
474
|
+
? ivParam
|
|
475
|
+
: ivParam instanceof ArrayBuffer
|
|
476
|
+
? new Uint8Array(ivParam)
|
|
477
|
+
: new Uint8Array(ivParam.buffer, ivParam.byteOffset, ivParam.byteLength);
|
|
478
|
+
const ivLength = ivArray.length;
|
|
479
|
+
outputData = new Uint8Array(ivLength + encrypted.byteLength);
|
|
480
|
+
outputData.set(ivArray, 0);
|
|
481
|
+
outputData.set(encryptedArray, ivLength);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
outputData = encryptedArray;
|
|
485
|
+
}
|
|
486
|
+
if (output) {
|
|
487
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
488
|
+
if (!ivFile && SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower]) {
|
|
489
|
+
const ivParam = encryptParams.iv;
|
|
490
|
+
const ivArray = ivParam instanceof Uint8Array
|
|
491
|
+
? ivParam
|
|
492
|
+
: ivParam instanceof ArrayBuffer
|
|
493
|
+
? new Uint8Array(ivParam)
|
|
494
|
+
: new Uint8Array(ivParam.buffer, ivParam.byteOffset, ivParam.byteLength);
|
|
495
|
+
const ivPath = path.resolve(shell.cwd, output + '.iv');
|
|
496
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, ivPath, ivArray);
|
|
497
|
+
await writelnStdout(process, terminal, chalk.yellow(`IV saved to ${output}.iv`));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
if (process?.stdout) {
|
|
502
|
+
const writer = process.stdout.getWriter();
|
|
503
|
+
try {
|
|
504
|
+
await writer.write(outputData);
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
writer.releaseLock();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
await writelnStderr(process, terminal, `crypto encrypt: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
518
|
+
return 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function handleDecrypt(shell, terminal, process, args) {
|
|
522
|
+
const usage = `Usage: crypto decrypt [OPTIONS]
|
|
523
|
+
|
|
524
|
+
Decrypt data using various algorithms.
|
|
525
|
+
|
|
526
|
+
Options:
|
|
527
|
+
--algorithm, -a ALGORITHM Algorithm (AES-GCM, AES-CBC, AES-CTR, RSA-OAEP)
|
|
528
|
+
--key-file, -k FILE Key file (JWK format)
|
|
529
|
+
--input, -i FILE Input file (default: stdin)
|
|
530
|
+
--output, -o FILE Output file (default: stdout)
|
|
531
|
+
--iv-file FILE IV/nonce file (for AES, required if not embedded)
|
|
532
|
+
--help Display this help`;
|
|
533
|
+
const parsed = parseArgs(args);
|
|
534
|
+
if (parsed.help) {
|
|
535
|
+
await writelnStderr(process, terminal, usage);
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
539
|
+
const keyFile = parsed['key-file'] || parsed.k;
|
|
540
|
+
const input = parsed.input || parsed.i;
|
|
541
|
+
const output = parsed.output || parsed.o;
|
|
542
|
+
const ivFile = parsed['iv-file'];
|
|
543
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
544
|
+
await writelnStderr(process, terminal, 'crypto decrypt: --algorithm is required');
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
548
|
+
await writelnStderr(process, terminal, 'crypto decrypt: --key-file is required');
|
|
549
|
+
return 1;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const algoLower = algorithm.toLowerCase();
|
|
553
|
+
if (!SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower] && algoLower !== 'rsa-oaep') {
|
|
554
|
+
await writelnStderr(process, terminal, `crypto decrypt: unsupported algorithm '${algorithm}'`);
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
const keyPath = path.resolve(shell.cwd, keyFile);
|
|
558
|
+
const keyData = await readFileToUint8Array(shell.context.fs.promises, keyPath);
|
|
559
|
+
const keyJson = JSON.parse(new TextDecoder().decode(keyData));
|
|
560
|
+
let inputData;
|
|
561
|
+
if (input) {
|
|
562
|
+
inputData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, input));
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (!process?.stdin) {
|
|
566
|
+
await writelnStderr(process, terminal, 'crypto decrypt: no input specified');
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
569
|
+
const reader = process.stdin.getReader();
|
|
570
|
+
inputData = await readStreamToUint8Array(reader);
|
|
571
|
+
}
|
|
572
|
+
let key;
|
|
573
|
+
let decryptParams;
|
|
574
|
+
let encryptedData;
|
|
575
|
+
if (SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower]) {
|
|
576
|
+
const symAlgo = SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower];
|
|
577
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: symAlgo }, false, ['decrypt']);
|
|
578
|
+
let iv;
|
|
579
|
+
if (ivFile && typeof ivFile === 'string') {
|
|
580
|
+
iv = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, ivFile));
|
|
581
|
+
encryptedData = inputData;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
if (symAlgo === 'AES-GCM') {
|
|
585
|
+
iv = inputData.slice(0, 12);
|
|
586
|
+
encryptedData = inputData.slice(12);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
iv = inputData.slice(0, 16);
|
|
590
|
+
encryptedData = inputData.slice(16);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const ivBuffer = toArrayBuffer(iv.buffer);
|
|
594
|
+
if (symAlgo === 'AES-GCM') {
|
|
595
|
+
decryptParams = { name: 'AES-GCM', iv: new Uint8Array(ivBuffer, iv.byteOffset, iv.length) };
|
|
596
|
+
}
|
|
597
|
+
else if (symAlgo === 'AES-CBC') {
|
|
598
|
+
decryptParams = { name: 'AES-CBC', iv: new Uint8Array(ivBuffer, iv.byteOffset, iv.length) };
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
decryptParams = { name: 'AES-CTR', counter: new Uint8Array(ivBuffer, iv.byteOffset, iv.length), length: 128 };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['decrypt']);
|
|
606
|
+
decryptParams = { name: 'RSA-OAEP' };
|
|
607
|
+
encryptedData = inputData;
|
|
608
|
+
}
|
|
609
|
+
const encryptedBuffer = toArrayBuffer(encryptedData.buffer);
|
|
610
|
+
const decrypted = await crypto.subtle.decrypt(decryptParams, key, new Uint8Array(encryptedBuffer, encryptedData.byteOffset, encryptedData.length));
|
|
611
|
+
const outputData = new Uint8Array(decrypted);
|
|
612
|
+
if (output) {
|
|
613
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
if (process?.stdout) {
|
|
617
|
+
const writer = process.stdout.getWriter();
|
|
618
|
+
try {
|
|
619
|
+
await writer.write(outputData);
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
writer.releaseLock();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return 0;
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
await writelnStderr(process, terminal, `crypto decrypt: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
633
|
+
return 1;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function handleSign(shell, terminal, process, args) {
|
|
637
|
+
const usage = `Usage: crypto sign [OPTIONS]
|
|
638
|
+
|
|
639
|
+
Sign data using various algorithms.
|
|
640
|
+
|
|
641
|
+
Options:
|
|
642
|
+
--algorithm, -a ALGORITHM Algorithm (ECDSA, RSA-PSS, RSASSA-PKCS1-v1_5, HMAC)
|
|
643
|
+
--key-file, -k FILE Private key file (JWK format)
|
|
644
|
+
--input, -i FILE Input file (default: stdin)
|
|
645
|
+
--output, -o FILE Output file (default: stdout)
|
|
646
|
+
--help Display this help
|
|
647
|
+
|
|
648
|
+
Examples:
|
|
649
|
+
crypto sign --algorithm ecdsa --key-file ecdsa-key.json --input message.txt --output signature.sig
|
|
650
|
+
crypto sign --algorithm hmac --key-file hmac-key.json --input data.bin --output signature.sig`;
|
|
651
|
+
const parsed = parseArgs(args);
|
|
652
|
+
if (parsed.help) {
|
|
653
|
+
await writelnStderr(process, terminal, usage);
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
657
|
+
const keyFile = parsed['key-file'] || parsed.k;
|
|
658
|
+
const input = parsed.input || parsed.i;
|
|
659
|
+
const output = parsed.output || parsed.o;
|
|
660
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
661
|
+
await writelnStderr(process, terminal, 'crypto sign: --algorithm is required');
|
|
662
|
+
return 1;
|
|
663
|
+
}
|
|
664
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
665
|
+
await writelnStderr(process, terminal, 'crypto sign: --key-file is required');
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const algoLower = algorithm.toLowerCase();
|
|
670
|
+
if (!SUPPORTED_SIGN_ALGORITHMS[algoLower]) {
|
|
671
|
+
await writelnStderr(process, terminal, `crypto sign: unsupported algorithm '${algorithm}'`);
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
const keyPath = path.resolve(shell.cwd, keyFile);
|
|
675
|
+
const keyData = await readFileToUint8Array(shell.context.fs.promises, keyPath);
|
|
676
|
+
const keyJson = JSON.parse(new TextDecoder().decode(keyData));
|
|
677
|
+
let key;
|
|
678
|
+
let signParams;
|
|
679
|
+
if (algoLower === 'ecdsa') {
|
|
680
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
681
|
+
signParams = { name: 'ECDSA', hash: 'SHA-256' };
|
|
682
|
+
}
|
|
683
|
+
else if (algoLower === 'rsa-pss') {
|
|
684
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSA-PSS', hash: 'SHA-256' }, false, ['sign']);
|
|
685
|
+
signParams = { name: 'RSA-PSS', saltLength: 32 };
|
|
686
|
+
}
|
|
687
|
+
else if (algoLower === 'rsassa-pkcs1-v1_5' || algoLower === 'rsassa-pkcs1-v1-5') {
|
|
688
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
|
|
689
|
+
signParams = { name: 'RSASSA-PKCS1-v1_5' };
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
693
|
+
signParams = { name: 'HMAC' };
|
|
694
|
+
}
|
|
695
|
+
let inputData;
|
|
696
|
+
if (input) {
|
|
697
|
+
inputData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, input));
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
if (!process?.stdin) {
|
|
701
|
+
await writelnStderr(process, terminal, 'crypto sign: no input specified');
|
|
702
|
+
return 1;
|
|
703
|
+
}
|
|
704
|
+
const reader = process.stdin.getReader();
|
|
705
|
+
inputData = await readStreamToUint8Array(reader);
|
|
706
|
+
}
|
|
707
|
+
const inputBuffer = toArrayBuffer(inputData.buffer);
|
|
708
|
+
const signature = await crypto.subtle.sign(signParams, key, new Uint8Array(inputBuffer, inputData.byteOffset, inputData.length));
|
|
709
|
+
const outputData = new Uint8Array(signature);
|
|
710
|
+
if (output) {
|
|
711
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
if (process?.stdout) {
|
|
715
|
+
const writer = process.stdout.getWriter();
|
|
716
|
+
try {
|
|
717
|
+
await writer.write(outputData);
|
|
718
|
+
}
|
|
719
|
+
finally {
|
|
720
|
+
writer.releaseLock();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return 0;
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
await writelnStderr(process, terminal, `crypto sign: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
731
|
+
return 1;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function handleVerify(shell, terminal, process, args) {
|
|
735
|
+
const usage = `Usage: crypto verify [OPTIONS]
|
|
736
|
+
|
|
737
|
+
Verify signatures using various algorithms.
|
|
738
|
+
|
|
739
|
+
Options:
|
|
740
|
+
--algorithm, -a ALGORITHM Algorithm (ECDSA, RSA-PSS, RSASSA-PKCS1-v1_5, HMAC)
|
|
741
|
+
--key-file, -k FILE Public key file (JWK format, can use key pair file)
|
|
742
|
+
--input, -i FILE Input file (default: stdin)
|
|
743
|
+
--signature, -s FILE Signature file
|
|
744
|
+
--help Display this help
|
|
745
|
+
|
|
746
|
+
Examples:
|
|
747
|
+
crypto verify --algorithm ecdsa --key-file ecdsa-key.json --input message.txt --signature signature.sig
|
|
748
|
+
crypto verify --algorithm hmac --key-file hmac-key.json --input data.bin --signature signature.sig`;
|
|
749
|
+
const parsed = parseArgs(args);
|
|
750
|
+
if (parsed.help) {
|
|
751
|
+
await writelnStderr(process, terminal, usage);
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
755
|
+
const keyFile = parsed['key-file'] || parsed.k;
|
|
756
|
+
const input = parsed.input || parsed.i;
|
|
757
|
+
const signatureFile = parsed.signature || parsed.s;
|
|
758
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
759
|
+
await writelnStderr(process, terminal, 'crypto verify: --algorithm is required');
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
763
|
+
await writelnStderr(process, terminal, 'crypto verify: --key-file is required');
|
|
764
|
+
return 1;
|
|
765
|
+
}
|
|
766
|
+
if (!signatureFile || typeof signatureFile !== 'string') {
|
|
767
|
+
await writelnStderr(process, terminal, 'crypto verify: --signature is required');
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const algoLower = algorithm.toLowerCase();
|
|
772
|
+
if (!SUPPORTED_SIGN_ALGORITHMS[algoLower]) {
|
|
773
|
+
await writelnStderr(process, terminal, `crypto verify: unsupported algorithm '${algorithm}'`);
|
|
774
|
+
return 1;
|
|
775
|
+
}
|
|
776
|
+
const keyPath = path.resolve(shell.cwd, keyFile);
|
|
777
|
+
const keyData = await readFileToUint8Array(shell.context.fs.promises, keyPath);
|
|
778
|
+
let keyJson = JSON.parse(new TextDecoder().decode(keyData));
|
|
779
|
+
// Extract public key from private key JWK if needed
|
|
780
|
+
if (keyJson.d) {
|
|
781
|
+
const publicKeyJson = {
|
|
782
|
+
kty: keyJson.kty,
|
|
783
|
+
key_ops: ['verify'],
|
|
784
|
+
ext: keyJson.ext
|
|
785
|
+
};
|
|
786
|
+
if (keyJson.kty === 'EC') {
|
|
787
|
+
publicKeyJson.crv = keyJson.crv;
|
|
788
|
+
publicKeyJson.x = keyJson.x;
|
|
789
|
+
publicKeyJson.y = keyJson.y;
|
|
790
|
+
}
|
|
791
|
+
else if (keyJson.kty === 'RSA') {
|
|
792
|
+
;
|
|
793
|
+
publicKeyJson.n = keyJson.n;
|
|
794
|
+
publicKeyJson.e = keyJson.e;
|
|
795
|
+
}
|
|
796
|
+
keyJson = publicKeyJson;
|
|
797
|
+
}
|
|
798
|
+
let key;
|
|
799
|
+
let verifyParams;
|
|
800
|
+
if (algoLower === 'ecdsa') {
|
|
801
|
+
const namedCurve = (keyJson.crv || 'P-256');
|
|
802
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'ECDSA', namedCurve }, false, ['verify']);
|
|
803
|
+
verifyParams = { name: 'ECDSA', hash: 'SHA-256' };
|
|
804
|
+
}
|
|
805
|
+
else if (algoLower === 'rsa-pss') {
|
|
806
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSA-PSS', hash: 'SHA-256' }, false, ['verify']);
|
|
807
|
+
verifyParams = { name: 'RSA-PSS', saltLength: 32 };
|
|
808
|
+
}
|
|
809
|
+
else if (algoLower === 'rsassa-pkcs1-v1_5' || algoLower === 'rsassa-pkcs1-v1-5') {
|
|
810
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
|
|
811
|
+
verifyParams = { name: 'RSASSA-PKCS1-v1_5' };
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
key = await crypto.subtle.importKey('jwk', keyJson, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
815
|
+
verifyParams = { name: 'HMAC' };
|
|
816
|
+
}
|
|
817
|
+
let inputData;
|
|
818
|
+
if (input) {
|
|
819
|
+
inputData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, input));
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
if (!process?.stdin) {
|
|
823
|
+
await writelnStderr(process, terminal, 'crypto verify: no input specified');
|
|
824
|
+
return 1;
|
|
825
|
+
}
|
|
826
|
+
const reader = process.stdin.getReader();
|
|
827
|
+
inputData = await readStreamToUint8Array(reader);
|
|
828
|
+
}
|
|
829
|
+
const signature = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, signatureFile));
|
|
830
|
+
const signatureBuffer = toArrayBuffer(signature.buffer);
|
|
831
|
+
const inputBuffer = toArrayBuffer(inputData.buffer);
|
|
832
|
+
const isValid = await crypto.subtle.verify(verifyParams, key, new Uint8Array(signatureBuffer, signature.byteOffset, signature.length), new Uint8Array(inputBuffer, inputData.byteOffset, inputData.length));
|
|
833
|
+
if (isValid) {
|
|
834
|
+
await writelnStdout(process, terminal, chalk.green('Signature is valid'));
|
|
835
|
+
return 0;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
await writelnStderr(process, terminal, chalk.red('Signature is invalid'));
|
|
839
|
+
return 1;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
await writelnStderr(process, terminal, `crypto verify: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
844
|
+
return 1;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function handleImport(shell, terminal, process, args) {
|
|
848
|
+
const usage = `Usage: crypto import [OPTIONS]
|
|
849
|
+
|
|
850
|
+
Import keys from various formats.
|
|
851
|
+
|
|
852
|
+
Options:
|
|
853
|
+
--format, -f FORMAT Input format (jwk, raw, pkcs8, spki)
|
|
854
|
+
--algorithm, -a ALGORITHM Algorithm (required for raw format)
|
|
855
|
+
--input, -i FILE Input file (default: stdin)
|
|
856
|
+
--output, -o FILE Output file (default: stdout)
|
|
857
|
+
--output-format FORMAT Output format (jwk, raw, pkcs8, spki) (default: jwk)
|
|
858
|
+
--help Display this help
|
|
859
|
+
|
|
860
|
+
Examples:
|
|
861
|
+
crypto import --format jwk --input key.json --output-format raw --output key.bin
|
|
862
|
+
crypto import --format pkcs8 --algorithm ecdsa --input key.pem --output-format jwk --output key.json
|
|
863
|
+
crypto import --format jwk --input key.json --output-format pkcs8 --output private.pem`;
|
|
864
|
+
const parsed = parseArgs(args);
|
|
865
|
+
if (parsed.help) {
|
|
866
|
+
await writelnStderr(process, terminal, usage);
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
const formatValue = parsed.format || parsed.f || 'jwk';
|
|
870
|
+
const format = (typeof formatValue === 'string' ? formatValue : 'jwk').toLowerCase();
|
|
871
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
872
|
+
const input = parsed.input || parsed.i;
|
|
873
|
+
const output = parsed.output || parsed.o;
|
|
874
|
+
const outputFormatValue = parsed['output-format'] || 'jwk';
|
|
875
|
+
const outputFormat = (typeof outputFormatValue === 'string' ? outputFormatValue : 'jwk').toLowerCase();
|
|
876
|
+
const keyFormat = SUPPORTED_KEY_FORMATS[format];
|
|
877
|
+
if (!keyFormat) {
|
|
878
|
+
await writelnStderr(process, terminal, `crypto import: unsupported input format '${format}'`);
|
|
879
|
+
return 1;
|
|
880
|
+
}
|
|
881
|
+
const outputKeyFormat = SUPPORTED_KEY_FORMATS[outputFormat];
|
|
882
|
+
if (!outputKeyFormat) {
|
|
883
|
+
await writelnStderr(process, terminal, `crypto import: unsupported output format '${outputFormat}'`);
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
886
|
+
if (keyFormat !== 'jwk' && (!algorithm || typeof algorithm !== 'string')) {
|
|
887
|
+
await writelnStderr(process, terminal, 'crypto import: --algorithm is required for non-JWK formats');
|
|
888
|
+
return 1;
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
let inputData;
|
|
892
|
+
if (input) {
|
|
893
|
+
inputData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, input));
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
if (!process?.stdin) {
|
|
897
|
+
await writelnStderr(process, terminal, 'crypto import: no input specified');
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
const reader = process.stdin.getReader();
|
|
901
|
+
inputData = await readStreamToUint8Array(reader);
|
|
902
|
+
}
|
|
903
|
+
let keyMaterial;
|
|
904
|
+
let importAlgorithm;
|
|
905
|
+
let keyUsages;
|
|
906
|
+
if (keyFormat === 'jwk') {
|
|
907
|
+
const json = new TextDecoder().decode(inputData);
|
|
908
|
+
const jwk = JSON.parse(json);
|
|
909
|
+
keyMaterial = jwk;
|
|
910
|
+
if (jwk.kty === 'RSA') {
|
|
911
|
+
importAlgorithm = { name: 'RSA-OAEP', hash: 'SHA-256' };
|
|
912
|
+
keyUsages = jwk.d ? ['decrypt', 'encrypt'] : ['encrypt'];
|
|
913
|
+
}
|
|
914
|
+
else if (jwk.kty === 'EC') {
|
|
915
|
+
importAlgorithm = { name: 'ECDSA', namedCurve: jwk.crv || 'P-256' };
|
|
916
|
+
keyUsages = jwk.d ? ['sign', 'verify'] : ['verify'];
|
|
917
|
+
}
|
|
918
|
+
else if (jwk.kty === 'oct') {
|
|
919
|
+
importAlgorithm = { name: 'AES-GCM' };
|
|
920
|
+
keyUsages = ['encrypt', 'decrypt'];
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
await writelnStderr(process, terminal, 'crypto import: unsupported key type in JWK');
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
keyMaterial = toArrayBuffer(inputData.buffer);
|
|
929
|
+
const algoLower = algorithm.toLowerCase();
|
|
930
|
+
if (SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower]) {
|
|
931
|
+
importAlgorithm = { name: SUPPORTED_SYMMETRIC_ALGORITHMS[algoLower] };
|
|
932
|
+
keyUsages = ['encrypt', 'decrypt'];
|
|
933
|
+
}
|
|
934
|
+
else if (SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDSA' || SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDH') {
|
|
935
|
+
importAlgorithm = { name: SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower], namedCurve: 'P-256' };
|
|
936
|
+
keyUsages = SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'ECDSA' ? ['sign', 'verify'] : ['deriveKey', 'deriveBits'];
|
|
937
|
+
}
|
|
938
|
+
else if (SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower]?.startsWith('RSA')) {
|
|
939
|
+
importAlgorithm = { name: SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower], hash: 'SHA-256' };
|
|
940
|
+
keyUsages = SUPPORTED_ASYMMETRIC_ALGORITHMS[algoLower] === 'RSA-OAEP' ? ['encrypt', 'decrypt'] : ['sign', 'verify'];
|
|
941
|
+
}
|
|
942
|
+
else if (algoLower === 'hmac') {
|
|
943
|
+
importAlgorithm = { name: 'HMAC', hash: 'SHA-256' };
|
|
944
|
+
keyUsages = ['sign', 'verify'];
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
await writelnStderr(process, terminal, `crypto import: unsupported algorithm '${algorithm}'`);
|
|
948
|
+
return 1;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const key = keyFormat === 'jwk'
|
|
952
|
+
? await crypto.subtle.importKey('jwk', keyMaterial, importAlgorithm, true, keyUsages)
|
|
953
|
+
: await crypto.subtle.importKey(keyFormat, keyMaterial, importAlgorithm, true, keyUsages);
|
|
954
|
+
let exported;
|
|
955
|
+
if ('publicKey' in key && 'privateKey' in key) {
|
|
956
|
+
const keyPair = key;
|
|
957
|
+
if (outputKeyFormat === 'spki') {
|
|
958
|
+
exported = await crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
959
|
+
}
|
|
960
|
+
else if (outputKeyFormat === 'pkcs8') {
|
|
961
|
+
exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
962
|
+
}
|
|
963
|
+
else if (outputKeyFormat === 'jwk') {
|
|
964
|
+
exported = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
await writelnStderr(process, terminal, 'crypto import: asymmetric keys cannot be exported in raw format');
|
|
968
|
+
return 1;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
if (outputKeyFormat === 'pkcs8' || outputKeyFormat === 'spki') {
|
|
973
|
+
await writelnStderr(process, terminal, `crypto import: symmetric keys cannot be exported in ${outputKeyFormat} format`);
|
|
974
|
+
return 1;
|
|
975
|
+
}
|
|
976
|
+
exported = await crypto.subtle.exportKey(outputKeyFormat, key);
|
|
977
|
+
}
|
|
978
|
+
let outputData;
|
|
979
|
+
if (outputKeyFormat === 'jwk') {
|
|
980
|
+
outputData = new TextEncoder().encode(JSON.stringify(exported, null, 2));
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
outputData = new Uint8Array(exported);
|
|
984
|
+
}
|
|
985
|
+
if (output) {
|
|
986
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
if (process?.stdout) {
|
|
990
|
+
const writer = process.stdout.getWriter();
|
|
991
|
+
try {
|
|
992
|
+
await writer.write(outputData);
|
|
993
|
+
if (outputKeyFormat === 'jwk') {
|
|
994
|
+
await writer.write(new TextEncoder().encode('\n'));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
finally {
|
|
998
|
+
writer.releaseLock();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return 0;
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
await writelnStderr(process, terminal, `crypto import: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1009
|
+
return 1;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async function handleExport(shell, terminal, process, args) {
|
|
1013
|
+
const usage = `Usage: crypto export [OPTIONS]
|
|
1014
|
+
|
|
1015
|
+
Export keys to various formats.
|
|
1016
|
+
|
|
1017
|
+
Options:
|
|
1018
|
+
--key-file, -k FILE Key file (JWK format)
|
|
1019
|
+
--format, -f FORMAT Output format (jwk, raw, pkcs8, spki) (default: jwk)
|
|
1020
|
+
--output, -o FILE Output file (default: stdout)
|
|
1021
|
+
--help Display this help`;
|
|
1022
|
+
const parsed = parseArgs(args);
|
|
1023
|
+
if (parsed.help) {
|
|
1024
|
+
await writelnStderr(process, terminal, usage);
|
|
1025
|
+
return 0;
|
|
1026
|
+
}
|
|
1027
|
+
const keyFile = parsed['key-file'] || parsed.k;
|
|
1028
|
+
const formatValue = parsed.format || parsed.f || 'jwk';
|
|
1029
|
+
const format = (typeof formatValue === 'string' ? formatValue : 'jwk').toLowerCase();
|
|
1030
|
+
const output = parsed.output || parsed.o;
|
|
1031
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
1032
|
+
await writelnStderr(process, terminal, 'crypto export: --key-file is required');
|
|
1033
|
+
return 1;
|
|
1034
|
+
}
|
|
1035
|
+
const keyFormat = SUPPORTED_KEY_FORMATS[format];
|
|
1036
|
+
if (!keyFormat) {
|
|
1037
|
+
await writelnStderr(process, terminal, `crypto export: unsupported format '${format}'`);
|
|
1038
|
+
return 1;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const keyPath = path.resolve(shell.cwd, keyFile);
|
|
1042
|
+
const keyData = await readFileToUint8Array(shell.context.fs.promises, keyPath);
|
|
1043
|
+
const keyJson = JSON.parse(new TextDecoder().decode(keyData));
|
|
1044
|
+
let importAlgorithm;
|
|
1045
|
+
let keyUsages;
|
|
1046
|
+
if (keyJson.kty === 'RSA') {
|
|
1047
|
+
importAlgorithm = { name: 'RSA-OAEP', hash: 'SHA-256' };
|
|
1048
|
+
keyUsages = keyJson.d ? ['decrypt', 'encrypt'] : ['encrypt'];
|
|
1049
|
+
}
|
|
1050
|
+
else if (keyJson.kty === 'EC') {
|
|
1051
|
+
importAlgorithm = { name: 'ECDSA', namedCurve: keyJson.crv || 'P-256' };
|
|
1052
|
+
keyUsages = keyJson.d ? ['sign', 'verify'] : ['verify'];
|
|
1053
|
+
}
|
|
1054
|
+
else if (keyJson.kty === 'oct') {
|
|
1055
|
+
importAlgorithm = { name: 'AES-GCM' };
|
|
1056
|
+
keyUsages = ['encrypt', 'decrypt'];
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
await writelnStderr(process, terminal, 'crypto export: unsupported key type in JWK');
|
|
1060
|
+
return 1;
|
|
1061
|
+
}
|
|
1062
|
+
const key = await crypto.subtle.importKey('jwk', keyJson, importAlgorithm, true, keyUsages);
|
|
1063
|
+
const exported = await crypto.subtle.exportKey(keyFormat, key);
|
|
1064
|
+
let outputData;
|
|
1065
|
+
if (keyFormat === 'jwk') {
|
|
1066
|
+
outputData = new TextEncoder().encode(JSON.stringify(exported, null, 2));
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
outputData = new Uint8Array(exported);
|
|
1070
|
+
}
|
|
1071
|
+
if (output) {
|
|
1072
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
if (process?.stdout) {
|
|
1076
|
+
const writer = process.stdout.getWriter();
|
|
1077
|
+
try {
|
|
1078
|
+
await writer.write(outputData);
|
|
1079
|
+
if (keyFormat === 'jwk') {
|
|
1080
|
+
await writer.write(new TextEncoder().encode('\n'));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
writer.releaseLock();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
await writelnStderr(process, terminal, `crypto export: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1095
|
+
return 1;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function handleDerive(shell, terminal, process, args) {
|
|
1099
|
+
const usage = `Usage: crypto derive [OPTIONS]
|
|
1100
|
+
|
|
1101
|
+
Derive keys from passwords or other keys.
|
|
1102
|
+
|
|
1103
|
+
Options:
|
|
1104
|
+
--algorithm, -a ALGORITHM Algorithm (PBKDF2, HKDF, ECDH)
|
|
1105
|
+
--password, -p PASSWORD Password (for PBKDF2/HKDF)
|
|
1106
|
+
--salt-file FILE Salt file (for PBKDF2/HKDF)
|
|
1107
|
+
--iterations, -i COUNT Iterations (for PBKDF2, default: 100000)
|
|
1108
|
+
--key-file FILE Private key file (for ECDH)
|
|
1109
|
+
--public-key-file FILE Public key file (for ECDH)
|
|
1110
|
+
--output, -o FILE Output file (default: stdout, JWK format)
|
|
1111
|
+
--help Display this help`;
|
|
1112
|
+
const parsed = parseArgs(args);
|
|
1113
|
+
if (parsed.help) {
|
|
1114
|
+
await writelnStderr(process, terminal, usage);
|
|
1115
|
+
return 0;
|
|
1116
|
+
}
|
|
1117
|
+
const algorithm = parsed.algorithm || parsed.a;
|
|
1118
|
+
const password = parsed.password || parsed.p;
|
|
1119
|
+
const saltFile = parsed['salt-file'];
|
|
1120
|
+
const iterations = parsed.iterations || parsed.i;
|
|
1121
|
+
const keyFile = parsed['key-file'];
|
|
1122
|
+
const publicKeyFile = parsed['public-key-file'];
|
|
1123
|
+
const output = parsed.output || parsed.o;
|
|
1124
|
+
if (!algorithm || typeof algorithm !== 'string') {
|
|
1125
|
+
await writelnStderr(process, terminal, 'crypto derive: --algorithm is required');
|
|
1126
|
+
return 1;
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
const algoLower = algorithm.toLowerCase();
|
|
1130
|
+
if (!SUPPORTED_DERIVE_ALGORITHMS[algoLower]) {
|
|
1131
|
+
await writelnStderr(process, terminal, `crypto derive: unsupported algorithm '${algorithm}'`);
|
|
1132
|
+
return 1;
|
|
1133
|
+
}
|
|
1134
|
+
let derivedKey;
|
|
1135
|
+
if (algoLower === 'pbkdf2') {
|
|
1136
|
+
if (!password || typeof password !== 'string') {
|
|
1137
|
+
await writelnStderr(process, terminal, 'crypto derive: --password is required for PBKDF2');
|
|
1138
|
+
return 1;
|
|
1139
|
+
}
|
|
1140
|
+
let salt;
|
|
1141
|
+
if (saltFile && typeof saltFile === 'string') {
|
|
1142
|
+
salt = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, saltFile));
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1146
|
+
}
|
|
1147
|
+
const passwordKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits', 'deriveKey']);
|
|
1148
|
+
const saltBuffer = toArrayBuffer(salt.buffer);
|
|
1149
|
+
derivedKey = await crypto.subtle.deriveKey({
|
|
1150
|
+
name: 'PBKDF2',
|
|
1151
|
+
salt: new Uint8Array(saltBuffer, salt.byteOffset, salt.length),
|
|
1152
|
+
iterations: iterations ? parseInt(iterations, 10) : 100000,
|
|
1153
|
+
hash: 'SHA-256'
|
|
1154
|
+
}, passwordKey, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
1155
|
+
}
|
|
1156
|
+
else if (algoLower === 'hkdf') {
|
|
1157
|
+
if (!password || typeof password !== 'string') {
|
|
1158
|
+
await writelnStderr(process, terminal, 'crypto derive: --password is required for HKDF');
|
|
1159
|
+
return 1;
|
|
1160
|
+
}
|
|
1161
|
+
let salt;
|
|
1162
|
+
if (saltFile && typeof saltFile === 'string') {
|
|
1163
|
+
salt = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, saltFile));
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1167
|
+
}
|
|
1168
|
+
const passwordKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'HKDF', false, ['deriveBits', 'deriveKey']);
|
|
1169
|
+
const saltBuffer = toArrayBuffer(salt.buffer);
|
|
1170
|
+
derivedKey = await crypto.subtle.deriveKey({
|
|
1171
|
+
name: 'HKDF',
|
|
1172
|
+
salt: new Uint8Array(saltBuffer, salt.byteOffset, salt.length),
|
|
1173
|
+
hash: 'SHA-256',
|
|
1174
|
+
info: new Uint8Array(0)
|
|
1175
|
+
}, passwordKey, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
if (!keyFile || typeof keyFile !== 'string') {
|
|
1179
|
+
await writelnStderr(process, terminal, 'crypto derive: --key-file is required for ECDH');
|
|
1180
|
+
return 1;
|
|
1181
|
+
}
|
|
1182
|
+
if (!publicKeyFile || typeof publicKeyFile !== 'string') {
|
|
1183
|
+
await writelnStderr(process, terminal, 'crypto derive: --public-key-file is required for ECDH');
|
|
1184
|
+
return 1;
|
|
1185
|
+
}
|
|
1186
|
+
const privateKeyData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, keyFile));
|
|
1187
|
+
const privateKeyJson = JSON.parse(new TextDecoder().decode(privateKeyData));
|
|
1188
|
+
const publicKeyData = await readFileToUint8Array(shell.context.fs.promises, path.resolve(shell.cwd, publicKeyFile));
|
|
1189
|
+
const publicKeyJson = JSON.parse(new TextDecoder().decode(publicKeyData));
|
|
1190
|
+
const privateKey = await crypto.subtle.importKey('jwk', privateKeyJson, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']);
|
|
1191
|
+
const publicKey = await crypto.subtle.importKey('jwk', publicKeyJson, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
1192
|
+
derivedKey = await crypto.subtle.deriveKey({ name: 'ECDH', public: publicKey }, privateKey, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
1193
|
+
}
|
|
1194
|
+
const exported = await crypto.subtle.exportKey('jwk', derivedKey);
|
|
1195
|
+
const outputData = new TextEncoder().encode(JSON.stringify(exported, null, 2));
|
|
1196
|
+
if (output) {
|
|
1197
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), outputData);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
if (process?.stdout) {
|
|
1201
|
+
const writer = process.stdout.getWriter();
|
|
1202
|
+
try {
|
|
1203
|
+
await writer.write(outputData);
|
|
1204
|
+
await writer.write(new TextEncoder().encode('\n'));
|
|
1205
|
+
}
|
|
1206
|
+
finally {
|
|
1207
|
+
writer.releaseLock();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
terminal.write(new TextDecoder().decode(outputData));
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return 0;
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
await writelnStderr(process, terminal, `crypto derive: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1218
|
+
return 1;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function handleRandom(shell, terminal, process, args) {
|
|
1222
|
+
const usage = `Usage: crypto random [OPTIONS]
|
|
1223
|
+
|
|
1224
|
+
Generate random bytes.
|
|
1225
|
+
|
|
1226
|
+
Options:
|
|
1227
|
+
--length, -l LENGTH Number of bytes to generate (default: 32)
|
|
1228
|
+
--output, -o FILE Output file (default: stdout)
|
|
1229
|
+
--help Display this help`;
|
|
1230
|
+
const parsed = parseArgs(args);
|
|
1231
|
+
if (parsed.help) {
|
|
1232
|
+
await writelnStderr(process, terminal, usage);
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
const length = parsed.length || parsed.l;
|
|
1236
|
+
const output = parsed.output || parsed.o;
|
|
1237
|
+
const byteLength = length ? parseInt(length, 10) : 32;
|
|
1238
|
+
if (isNaN(byteLength) || byteLength <= 0) {
|
|
1239
|
+
await writelnStderr(process, terminal, 'crypto random: --length must be a positive number');
|
|
1240
|
+
return 1;
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(byteLength));
|
|
1244
|
+
if (output) {
|
|
1245
|
+
await writeUint8ArrayToFile(shell.context.fs.promises, path.resolve(shell.cwd, output), randomBytes);
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (process?.stdout) {
|
|
1249
|
+
const writer = process.stdout.getWriter();
|
|
1250
|
+
try {
|
|
1251
|
+
await writer.write(randomBytes);
|
|
1252
|
+
}
|
|
1253
|
+
finally {
|
|
1254
|
+
writer.releaseLock();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
terminal.write(new TextDecoder().decode(randomBytes));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
await writelnStderr(process, terminal, `crypto random: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1265
|
+
return 1;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
export function createCommand(kernel, shell, terminal) {
|
|
1269
|
+
return new TerminalCommand({
|
|
1270
|
+
command: 'crypto',
|
|
1271
|
+
description: 'Cryptographic utilities using the Web Crypto API',
|
|
1272
|
+
kernel,
|
|
1273
|
+
shell,
|
|
1274
|
+
terminal,
|
|
1275
|
+
run: async (pid, argv) => {
|
|
1276
|
+
const process = kernel.processes.get(pid);
|
|
1277
|
+
if (!process)
|
|
1278
|
+
return 1;
|
|
1279
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
1280
|
+
printUsage(process, terminal);
|
|
1281
|
+
return 0;
|
|
1282
|
+
}
|
|
1283
|
+
if (argv.length === 0) {
|
|
1284
|
+
printUsage(process, terminal);
|
|
1285
|
+
return 0;
|
|
1286
|
+
}
|
|
1287
|
+
const subcommand = argv[0]?.toLowerCase();
|
|
1288
|
+
const subArgs = argv.slice(1);
|
|
1289
|
+
try {
|
|
1290
|
+
switch (subcommand) {
|
|
1291
|
+
case 'generate':
|
|
1292
|
+
return await handleGenerate(shell, terminal, process, subArgs);
|
|
1293
|
+
case 'encrypt':
|
|
1294
|
+
return await handleEncrypt(shell, terminal, process, subArgs);
|
|
1295
|
+
case 'decrypt':
|
|
1296
|
+
return await handleDecrypt(shell, terminal, process, subArgs);
|
|
1297
|
+
case 'sign':
|
|
1298
|
+
return await handleSign(shell, terminal, process, subArgs);
|
|
1299
|
+
case 'verify':
|
|
1300
|
+
return await handleVerify(shell, terminal, process, subArgs);
|
|
1301
|
+
case 'import':
|
|
1302
|
+
return await handleImport(shell, terminal, process, subArgs);
|
|
1303
|
+
case 'export':
|
|
1304
|
+
return await handleExport(shell, terminal, process, subArgs);
|
|
1305
|
+
case 'derive':
|
|
1306
|
+
return await handleDerive(shell, terminal, process, subArgs);
|
|
1307
|
+
case 'random':
|
|
1308
|
+
return await handleRandom(shell, terminal, process, subArgs);
|
|
1309
|
+
default:
|
|
1310
|
+
await writelnStderr(process, terminal, chalk.red(`Error: Unknown subcommand: ${subcommand}`));
|
|
1311
|
+
await writelnStderr(process, terminal, 'Run "crypto --help" for usage information');
|
|
1312
|
+
return 1;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
catch (error) {
|
|
1316
|
+
await writelnStderr(process, terminal, chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1317
|
+
return 1;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
//# sourceMappingURL=crypto.js.map
|