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