@gjsify/crypto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +27 -0
  2. package/lib/esm/asn1.js +504 -0
  3. package/lib/esm/bigint-math.js +34 -0
  4. package/lib/esm/cipher.js +1272 -0
  5. package/lib/esm/constants.js +15 -0
  6. package/lib/esm/crypto-utils.js +47 -0
  7. package/lib/esm/dh.js +411 -0
  8. package/lib/esm/ecdh.js +356 -0
  9. package/lib/esm/ecdsa.js +125 -0
  10. package/lib/esm/hash.js +100 -0
  11. package/lib/esm/hkdf.js +58 -0
  12. package/lib/esm/hmac.js +93 -0
  13. package/lib/esm/index.js +158 -0
  14. package/lib/esm/key-object.js +330 -0
  15. package/lib/esm/mgf1.js +27 -0
  16. package/lib/esm/pbkdf2.js +68 -0
  17. package/lib/esm/public-encrypt.js +175 -0
  18. package/lib/esm/random.js +138 -0
  19. package/lib/esm/rsa-oaep.js +95 -0
  20. package/lib/esm/rsa-pss.js +100 -0
  21. package/lib/esm/scrypt.js +134 -0
  22. package/lib/esm/sign.js +248 -0
  23. package/lib/esm/timing-safe-equal.js +13 -0
  24. package/lib/esm/x509.js +214 -0
  25. package/lib/types/asn1.d.ts +87 -0
  26. package/lib/types/bigint-math.d.ts +13 -0
  27. package/lib/types/cipher.d.ts +84 -0
  28. package/lib/types/constants.d.ts +10 -0
  29. package/lib/types/crypto-utils.d.ts +22 -0
  30. package/lib/types/dh.d.ts +79 -0
  31. package/lib/types/ecdh.d.ts +96 -0
  32. package/lib/types/ecdsa.d.ts +21 -0
  33. package/lib/types/hash.d.ts +25 -0
  34. package/lib/types/hkdf.d.ts +9 -0
  35. package/lib/types/hmac.d.ts +20 -0
  36. package/lib/types/index.d.ts +105 -0
  37. package/lib/types/key-object.d.ts +36 -0
  38. package/lib/types/mgf1.d.ts +5 -0
  39. package/lib/types/pbkdf2.d.ts +9 -0
  40. package/lib/types/public-encrypt.d.ts +42 -0
  41. package/lib/types/random.d.ts +22 -0
  42. package/lib/types/rsa-oaep.d.ts +8 -0
  43. package/lib/types/rsa-pss.d.ts +8 -0
  44. package/lib/types/scrypt.d.ts +11 -0
  45. package/lib/types/sign.d.ts +61 -0
  46. package/lib/types/timing-safe-equal.d.ts +6 -0
  47. package/lib/types/x509.d.ts +72 -0
  48. package/package.json +45 -0
  49. package/src/asn1.ts +797 -0
  50. package/src/bigint-math.ts +45 -0
  51. package/src/cipher.spec.ts +332 -0
  52. package/src/cipher.ts +952 -0
  53. package/src/constants.ts +16 -0
  54. package/src/crypto-utils.ts +64 -0
  55. package/src/dh.spec.ts +111 -0
  56. package/src/dh.ts +761 -0
  57. package/src/ecdh.spec.ts +116 -0
  58. package/src/ecdh.ts +624 -0
  59. package/src/ecdsa.ts +243 -0
  60. package/src/extended.spec.ts +444 -0
  61. package/src/gcm.spec.ts +141 -0
  62. package/src/hash.spec.ts +86 -0
  63. package/src/hash.ts +119 -0
  64. package/src/hkdf.ts +99 -0
  65. package/src/hmac.spec.ts +64 -0
  66. package/src/hmac.ts +123 -0
  67. package/src/index.ts +93 -0
  68. package/src/key-object.spec.ts +202 -0
  69. package/src/key-object.ts +401 -0
  70. package/src/mgf1.ts +37 -0
  71. package/src/pbkdf2.spec.ts +76 -0
  72. package/src/pbkdf2.ts +106 -0
  73. package/src/public-encrypt.ts +288 -0
  74. package/src/random.spec.ts +133 -0
  75. package/src/random.ts +183 -0
  76. package/src/rsa-oaep.ts +167 -0
  77. package/src/rsa-pss.ts +190 -0
  78. package/src/scrypt.spec.ts +90 -0
  79. package/src/scrypt.ts +191 -0
  80. package/src/sign.spec.ts +160 -0
  81. package/src/sign.ts +319 -0
  82. package/src/test.mts +19 -0
  83. package/src/timing-safe-equal.ts +21 -0
  84. package/src/x509.spec.ts +210 -0
  85. package/src/x509.ts +262 -0
  86. package/tsconfig.json +31 -0
  87. package/tsconfig.tsbuildinfo +1 -0
package/src/asn1.ts ADDED
@@ -0,0 +1,797 @@
1
+ // ASN.1/DER/PEM parser for crypto keys — original implementation for GJS
2
+ // Reference: refs/browserify-sign/browser/sign.js (parse-asn1 patterns)
3
+
4
+ import { Buffer } from 'node:buffer';
5
+
6
+ // ============================================================================
7
+ // PEM parsing
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Strip PEM armor (header/footer lines), base64-decode the body to DER bytes.
12
+ */
13
+ function pemToDer(pem: string): { type: string; der: Uint8Array } {
14
+ const lines = pem.trim().split(/\r?\n/);
15
+
16
+ // Find header line
17
+ const headerIdx = lines.findIndex((l) => l.startsWith('-----BEGIN '));
18
+ if (headerIdx === -1) {
19
+ throw new Error('Invalid PEM: no BEGIN line found');
20
+ }
21
+ const headerLine = lines[headerIdx];
22
+ const headerMatch = headerLine.match(/^-----BEGIN (.+)-----$/);
23
+ if (!headerMatch) {
24
+ throw new Error('Invalid PEM header format');
25
+ }
26
+ const type = headerMatch[1];
27
+
28
+ // Find footer line
29
+ const footerIdx = lines.findIndex((l, i) => i > headerIdx && l.startsWith('-----END '));
30
+ if (footerIdx === -1) {
31
+ throw new Error('Invalid PEM: no END line found');
32
+ }
33
+
34
+ // Base64-decode the body between header and footer
35
+ const base64Body = lines.slice(headerIdx + 1, footerIdx).join('');
36
+ const der = Buffer.from(base64Body, 'base64');
37
+
38
+ return { type, der: new Uint8Array(der.buffer, der.byteOffset, der.byteLength) };
39
+ }
40
+
41
+ // ============================================================================
42
+ // DER / ASN.1 parser
43
+ // ============================================================================
44
+
45
+ /** ASN.1 tag constants */
46
+ const ASN1_INTEGER = 0x02;
47
+ const ASN1_BIT_STRING = 0x03;
48
+ const ASN1_OCTET_STRING = 0x04;
49
+ const ASN1_NULL = 0x05;
50
+ const ASN1_OID = 0x06;
51
+ const ASN1_SEQUENCE = 0x30;
52
+
53
+ interface DerValue {
54
+ tag: number;
55
+ data: Uint8Array;
56
+ children?: DerValue[];
57
+ }
58
+
59
+ /**
60
+ * Parse one TLV (tag-length-value) from the DER buffer starting at `offset`.
61
+ * Returns the parsed value and the new offset past it.
62
+ */
63
+ function parseTlv(buf: Uint8Array, offset: number): { value: DerValue; next: number } {
64
+ if (offset >= buf.length) {
65
+ throw new Error('ASN.1 parse error: unexpected end of data');
66
+ }
67
+
68
+ const tag = buf[offset++];
69
+
70
+ // Parse length
71
+ let length: number;
72
+ const firstLenByte = buf[offset++];
73
+ if (firstLenByte < 0x80) {
74
+ // Short form
75
+ length = firstLenByte;
76
+ } else {
77
+ // Long form: firstLenByte & 0x7f = number of subsequent length bytes
78
+ const numLenBytes = firstLenByte & 0x7f;
79
+ if (numLenBytes === 0) {
80
+ throw new Error('ASN.1 parse error: indefinite length not supported');
81
+ }
82
+ length = 0;
83
+ for (let i = 0; i < numLenBytes; i++) {
84
+ length = (length << 8) | buf[offset++];
85
+ }
86
+ }
87
+
88
+ const data = buf.slice(offset, offset + length);
89
+ const next = offset + length;
90
+
91
+ const result: DerValue = { tag, data };
92
+
93
+ // If the tag is a SEQUENCE (constructed), parse children
94
+ if (tag === ASN1_SEQUENCE) {
95
+ result.children = parseSequenceChildren(data);
96
+ }
97
+
98
+ return { value: result, next };
99
+ }
100
+
101
+ /**
102
+ * Parse all TLV children within a SEQUENCE's data bytes.
103
+ */
104
+ function parseSequenceChildren(data: Uint8Array): DerValue[] {
105
+ const children: DerValue[] = [];
106
+ let pos = 0;
107
+ while (pos < data.length) {
108
+ const { value, next } = parseTlv(data, pos);
109
+ children.push(value);
110
+ pos = next;
111
+ }
112
+ return children;
113
+ }
114
+
115
+ /**
116
+ * Parse the entire DER buffer as a single top-level TLV.
117
+ */
118
+ function parseDer(buf: Uint8Array): DerValue {
119
+ const { value } = parseTlv(buf, 0);
120
+ return value;
121
+ }
122
+
123
+ // ============================================================================
124
+ // Integer extraction
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Convert an ASN.1 INTEGER's raw data bytes to a non-negative BigInt.
129
+ * ASN.1 INTEGERs are big-endian two's-complement. For RSA keys all values
130
+ * are positive, so we just strip any leading 0x00 padding byte.
131
+ */
132
+ function integerToBigInt(data: Uint8Array): bigint {
133
+ let start = 0;
134
+ // Strip leading zero byte used to keep the integer positive
135
+ while (start < data.length - 1 && data[start] === 0) {
136
+ start++;
137
+ }
138
+ let result = 0n;
139
+ for (let i = start; i < data.length; i++) {
140
+ result = (result << 8n) | BigInt(data[i]);
141
+ }
142
+ return result;
143
+ }
144
+
145
+ // ============================================================================
146
+ // OID matching
147
+ // ============================================================================
148
+
149
+ /** RSA encryption OID: 1.2.840.113549.1.1.1 */
150
+ const RSA_OID = new Uint8Array([0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]);
151
+
152
+ function oidsEqual(a: Uint8Array, b: Uint8Array): boolean {
153
+ if (a.length !== b.length) return false;
154
+ for (let i = 0; i < a.length; i++) {
155
+ if (a[i] !== b[i]) return false;
156
+ }
157
+ return true;
158
+ }
159
+
160
+ // ============================================================================
161
+ // Key component types
162
+ // ============================================================================
163
+
164
+ export interface RsaPublicComponents {
165
+ n: bigint;
166
+ e: bigint;
167
+ }
168
+
169
+ export interface RsaPrivateComponents {
170
+ n: bigint;
171
+ e: bigint;
172
+ d: bigint;
173
+ p: bigint;
174
+ q: bigint;
175
+ }
176
+
177
+ export type ParsedKey =
178
+ | { type: 'rsa-public'; components: RsaPublicComponents }
179
+ | { type: 'rsa-private'; components: RsaPrivateComponents };
180
+
181
+ // ============================================================================
182
+ // PKCS#1 RSAPublicKey / RSAPrivateKey
183
+ // ============================================================================
184
+
185
+ /**
186
+ * Parse PKCS#1 RSAPublicKey:
187
+ * SEQUENCE { INTEGER n, INTEGER e }
188
+ */
189
+ function parseRsaPublicKeyPkcs1(seq: DerValue): RsaPublicComponents {
190
+ const children = seq.children;
191
+ if (!children || children.length < 2) {
192
+ throw new Error('Invalid PKCS#1 RSAPublicKey structure');
193
+ }
194
+ return {
195
+ n: integerToBigInt(children[0].data),
196
+ e: integerToBigInt(children[1].data),
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Parse PKCS#1 RSAPrivateKey:
202
+ * SEQUENCE {
203
+ * INTEGER version,
204
+ * INTEGER n,
205
+ * INTEGER e,
206
+ * INTEGER d,
207
+ * INTEGER p,
208
+ * INTEGER q,
209
+ * INTEGER dp,
210
+ * INTEGER dq,
211
+ * INTEGER qi
212
+ * }
213
+ */
214
+ function parseRsaPrivateKeyPkcs1(seq: DerValue): RsaPrivateComponents {
215
+ const children = seq.children;
216
+ if (!children || children.length < 6) {
217
+ throw new Error('Invalid PKCS#1 RSAPrivateKey structure');
218
+ }
219
+ // children[0] = version (should be 0)
220
+ return {
221
+ n: integerToBigInt(children[1].data),
222
+ e: integerToBigInt(children[2].data),
223
+ d: integerToBigInt(children[3].data),
224
+ p: integerToBigInt(children[4].data),
225
+ q: integerToBigInt(children[5].data),
226
+ };
227
+ }
228
+
229
+ // ============================================================================
230
+ // PKCS#8 SubjectPublicKeyInfo / PrivateKeyInfo
231
+ // ============================================================================
232
+
233
+ /**
234
+ * Parse PKCS#8 SubjectPublicKeyInfo:
235
+ * SEQUENCE {
236
+ * SEQUENCE { OID algorithm, NULL } -- AlgorithmIdentifier
237
+ * BIT STRING -- wraps PKCS#1 RSAPublicKey
238
+ * }
239
+ */
240
+ function parseSubjectPublicKeyInfo(seq: DerValue): RsaPublicComponents {
241
+ const children = seq.children;
242
+ if (!children || children.length < 2) {
243
+ throw new Error('Invalid SubjectPublicKeyInfo structure');
244
+ }
245
+
246
+ // Verify algorithm OID is RSA
247
+ const algIdSeq = children[0];
248
+ if (!algIdSeq.children || algIdSeq.children.length < 1) {
249
+ throw new Error('Invalid AlgorithmIdentifier');
250
+ }
251
+ const oid = algIdSeq.children[0];
252
+ if (oid.tag !== ASN1_OID || !oidsEqual(oid.data, RSA_OID)) {
253
+ throw new Error('Unsupported algorithm: only RSA is supported');
254
+ }
255
+
256
+ // The BIT STRING wraps the PKCS#1 RSAPublicKey
257
+ const bitString = children[1];
258
+ if (bitString.tag !== ASN1_BIT_STRING) {
259
+ throw new Error('Expected BIT STRING for public key data');
260
+ }
261
+ // BIT STRING has a leading byte for unused-bits count (should be 0)
262
+ const innerDer = bitString.data.slice(1);
263
+ const innerSeq = parseDer(innerDer);
264
+ return parseRsaPublicKeyPkcs1(innerSeq);
265
+ }
266
+
267
+ /**
268
+ * Parse PKCS#8 PrivateKeyInfo:
269
+ * SEQUENCE {
270
+ * INTEGER version,
271
+ * SEQUENCE { OID algorithm, NULL } -- AlgorithmIdentifier
272
+ * OCTET STRING -- wraps PKCS#1 RSAPrivateKey
273
+ * }
274
+ */
275
+ function parsePrivateKeyInfo(seq: DerValue): RsaPrivateComponents {
276
+ const children = seq.children;
277
+ if (!children || children.length < 3) {
278
+ throw new Error('Invalid PrivateKeyInfo structure');
279
+ }
280
+
281
+ // children[0] = version INTEGER (should be 0)
282
+ // Verify algorithm OID is RSA
283
+ const algIdSeq = children[1];
284
+ if (!algIdSeq.children || algIdSeq.children.length < 1) {
285
+ throw new Error('Invalid AlgorithmIdentifier');
286
+ }
287
+ const oid = algIdSeq.children[0];
288
+ if (oid.tag !== ASN1_OID || !oidsEqual(oid.data, RSA_OID)) {
289
+ throw new Error('Unsupported algorithm: only RSA is supported');
290
+ }
291
+
292
+ // The OCTET STRING wraps the PKCS#1 RSAPrivateKey
293
+ const octetString = children[2];
294
+ if (octetString.tag !== ASN1_OCTET_STRING) {
295
+ throw new Error('Expected OCTET STRING for private key data');
296
+ }
297
+ const innerSeq = parseDer(octetString.data);
298
+ return parseRsaPrivateKeyPkcs1(innerSeq);
299
+ }
300
+
301
+ // ============================================================================
302
+ // DER encoder
303
+ // ============================================================================
304
+
305
+ /**
306
+ * Encode a length value in DER format.
307
+ */
308
+ function encodeLength(length: number): Uint8Array {
309
+ if (length < 0x80) {
310
+ return new Uint8Array([length]);
311
+ }
312
+ const bytes: number[] = [];
313
+ let val = length;
314
+ while (val > 0) {
315
+ bytes.unshift(val & 0xff);
316
+ val >>= 8;
317
+ }
318
+ return new Uint8Array([0x80 | bytes.length, ...bytes]);
319
+ }
320
+
321
+ /**
322
+ * Encode a TLV (tag-length-value).
323
+ */
324
+ function encodeTlv(tag: number, data: Uint8Array): Uint8Array {
325
+ const len = encodeLength(data.length);
326
+ const result = new Uint8Array(1 + len.length + data.length);
327
+ result[0] = tag;
328
+ result.set(len, 1);
329
+ result.set(data, 1 + len.length);
330
+ return result;
331
+ }
332
+
333
+ /**
334
+ * Encode a BigInt as an ASN.1 INTEGER.
335
+ */
336
+ export function bigintToAsn1Integer(value: bigint): Uint8Array {
337
+ if (value === 0n) {
338
+ return encodeTlv(ASN1_INTEGER, new Uint8Array([0]));
339
+ }
340
+ const hex = value.toString(16);
341
+ const paddedHex = hex.length % 2 ? '0' + hex : hex;
342
+ const bytes: number[] = [];
343
+ for (let i = 0; i < paddedHex.length; i += 2) {
344
+ bytes.push(parseInt(paddedHex.substring(i, i + 2), 16));
345
+ }
346
+ // Add leading 0x00 if high bit is set (to keep integer positive)
347
+ if (bytes[0] & 0x80) {
348
+ bytes.unshift(0);
349
+ }
350
+ return encodeTlv(ASN1_INTEGER, new Uint8Array(bytes));
351
+ }
352
+
353
+ /**
354
+ * Encode an ASN.1 SEQUENCE from its children.
355
+ */
356
+ export function encodeSequence(children: Uint8Array[]): Uint8Array {
357
+ let totalLen = 0;
358
+ for (const child of children) totalLen += child.length;
359
+ const data = new Uint8Array(totalLen);
360
+ let offset = 0;
361
+ for (const child of children) {
362
+ data.set(child, offset);
363
+ offset += child.length;
364
+ }
365
+ return encodeTlv(ASN1_SEQUENCE, data);
366
+ }
367
+
368
+ /**
369
+ * Encode an ASN.1 BIT STRING (with 0 unused bits).
370
+ */
371
+ function encodeBitString(data: Uint8Array): Uint8Array {
372
+ const inner = new Uint8Array(1 + data.length);
373
+ inner[0] = 0; // 0 unused bits
374
+ inner.set(data, 1);
375
+ return encodeTlv(ASN1_BIT_STRING, inner);
376
+ }
377
+
378
+ /**
379
+ * Encode an ASN.1 OCTET STRING.
380
+ */
381
+ function encodeOctetString(data: Uint8Array): Uint8Array {
382
+ return encodeTlv(ASN1_OCTET_STRING, data);
383
+ }
384
+
385
+ /**
386
+ * Encode an ASN.1 OID.
387
+ */
388
+ function encodeOid(oidBytes: Uint8Array): Uint8Array {
389
+ return encodeTlv(ASN1_OID, oidBytes);
390
+ }
391
+
392
+ /**
393
+ * Encode ASN.1 NULL.
394
+ */
395
+ function encodeNull(): Uint8Array {
396
+ return new Uint8Array([ASN1_NULL, 0]);
397
+ }
398
+
399
+ /**
400
+ * Encode RSA public key components as PKCS#1 RSAPublicKey DER.
401
+ */
402
+ export function encodeRsaPublicKeyPkcs1(components: RsaPublicComponents): Uint8Array {
403
+ return encodeSequence([
404
+ bigintToAsn1Integer(components.n),
405
+ bigintToAsn1Integer(components.e),
406
+ ]);
407
+ }
408
+
409
+ /**
410
+ * Encode RSA public key as PKCS#8 SubjectPublicKeyInfo DER.
411
+ */
412
+ export function encodeSubjectPublicKeyInfo(components: RsaPublicComponents): Uint8Array {
413
+ const algorithmId = encodeSequence([encodeOid(RSA_OID), encodeNull()]);
414
+ const rsaPublicKey = encodeRsaPublicKeyPkcs1(components);
415
+ const bitString = encodeBitString(rsaPublicKey);
416
+ return encodeSequence([algorithmId, bitString]);
417
+ }
418
+
419
+ /**
420
+ * Encode RSA private key components as PKCS#1 RSAPrivateKey DER.
421
+ */
422
+ export function encodeRsaPrivateKeyPkcs1(components: RsaPrivateComponents): Uint8Array {
423
+ // Compute dp, dq, qi from p, q, d
424
+ const dp = components.d % (components.p - 1n);
425
+ const dq = components.d % (components.q - 1n);
426
+ const qi = modInverse(components.q, components.p);
427
+ return encodeSequence([
428
+ bigintToAsn1Integer(0n), // version
429
+ bigintToAsn1Integer(components.n),
430
+ bigintToAsn1Integer(components.e),
431
+ bigintToAsn1Integer(components.d),
432
+ bigintToAsn1Integer(components.p),
433
+ bigintToAsn1Integer(components.q),
434
+ bigintToAsn1Integer(dp),
435
+ bigintToAsn1Integer(dq),
436
+ bigintToAsn1Integer(qi),
437
+ ]);
438
+ }
439
+
440
+ /**
441
+ * Encode RSA private key as PKCS#8 PrivateKeyInfo DER.
442
+ */
443
+ export function encodePrivateKeyInfo(components: RsaPrivateComponents): Uint8Array {
444
+ const algorithmId = encodeSequence([encodeOid(RSA_OID), encodeNull()]);
445
+ const rsaPrivateKey = encodeRsaPrivateKeyPkcs1(components);
446
+ const octetString = encodeOctetString(rsaPrivateKey);
447
+ return encodeSequence([
448
+ bigintToAsn1Integer(0n), // version
449
+ algorithmId,
450
+ octetString,
451
+ ]);
452
+ }
453
+
454
+ /**
455
+ * Convert DER bytes to PEM string.
456
+ */
457
+ export function derToPem(der: Uint8Array, type: string): string {
458
+ const base64 = Buffer.from(der).toString('base64');
459
+ const lines: string[] = [`-----BEGIN ${type}-----`];
460
+ for (let i = 0; i < base64.length; i += 64) {
461
+ lines.push(base64.substring(i, i + 64));
462
+ }
463
+ lines.push(`-----END ${type}-----`);
464
+ return lines.join('\n');
465
+ }
466
+
467
+ /**
468
+ * Modular inverse: a^(-1) mod m using extended Euclidean algorithm.
469
+ */
470
+ function modInverse(a: bigint, m: bigint): bigint {
471
+ let [old_r, r] = [a % m, m];
472
+ let [old_s, s] = [1n, 0n];
473
+ while (r !== 0n) {
474
+ const q = old_r / r;
475
+ [old_r, r] = [r, old_r - q * r];
476
+ [old_s, s] = [s, old_s - q * s];
477
+ }
478
+ return ((old_s % m) + m) % m;
479
+ }
480
+
481
+ // ============================================================================
482
+ // X.509 Certificate parsing
483
+ // ============================================================================
484
+
485
+ export interface X509Components {
486
+ raw: Uint8Array;
487
+ tbsCertificate: Uint8Array;
488
+ serialNumber: bigint;
489
+ issuer: string;
490
+ subject: string;
491
+ validFrom: Date;
492
+ validTo: Date;
493
+ publicKey: RsaPublicComponents | null;
494
+ publicKeyAlgorithm: string;
495
+ signatureAlgorithm: string;
496
+ signature: Uint8Array;
497
+ subjectAltName?: string[];
498
+ extensions?: DerValue[];
499
+ }
500
+
501
+ /**
502
+ * Parse an X.509 certificate from PEM or DER.
503
+ */
504
+ export function parseX509(pem: string): X509Components {
505
+ const { der } = pemToDer(pem);
506
+ return parseX509Der(der);
507
+ }
508
+
509
+ /**
510
+ * Parse an X.509 certificate from DER bytes.
511
+ */
512
+ export function parseX509Der(der: Uint8Array): X509Components {
513
+ const root = parseDer(der);
514
+ if (root.tag !== ASN1_SEQUENCE || !root.children || root.children.length < 3) {
515
+ throw new Error('Invalid X.509 certificate structure');
516
+ }
517
+
518
+ const tbsCertificate = root.children[0];
519
+ const signatureAlgorithm = root.children[1];
520
+ const signatureBitString = root.children[2];
521
+
522
+ if (!tbsCertificate.children || tbsCertificate.children.length < 6) {
523
+ throw new Error('Invalid TBSCertificate structure');
524
+ }
525
+
526
+ let idx = 0;
527
+ // Check for explicit [0] version tag (0xa0)
528
+ if (tbsCertificate.children[0].tag === 0xa0) {
529
+ idx++; // skip version wrapper
530
+ }
531
+
532
+ // Serial number
533
+ const serialNumber = integerToBigInt(tbsCertificate.children[idx].data);
534
+ idx++;
535
+
536
+ // Signature algorithm inside TBS (skip, use outer one)
537
+ idx++;
538
+
539
+ // Issuer
540
+ const issuer = parseDN(tbsCertificate.children[idx]);
541
+ idx++;
542
+
543
+ // Validity
544
+ const validity = tbsCertificate.children[idx];
545
+ const validFrom = parseAsn1Time(validity.children![0]);
546
+ const validTo = parseAsn1Time(validity.children![1]);
547
+ idx++;
548
+
549
+ // Subject
550
+ const subject = parseDN(tbsCertificate.children[idx]);
551
+ idx++;
552
+
553
+ // Subject Public Key Info
554
+ let publicKey: RsaPublicComponents | null = null;
555
+ let publicKeyAlgorithm = 'unknown';
556
+ if (idx < tbsCertificate.children.length) {
557
+ const spki = tbsCertificate.children[idx];
558
+ try {
559
+ if (spki.children && spki.children.length >= 2) {
560
+ const algId = spki.children[0];
561
+ if (algId.children && algId.children.length >= 1) {
562
+ const oid = algId.children[0];
563
+ if (oid.tag === ASN1_OID && oidsEqual(oid.data, RSA_OID)) {
564
+ publicKeyAlgorithm = 'rsa';
565
+ publicKey = parseSubjectPublicKeyInfo(spki);
566
+ }
567
+ }
568
+ }
569
+ } catch {
570
+ // Non-RSA key or parse error — leave null
571
+ }
572
+ idx++;
573
+ }
574
+
575
+ // Parse extensions (optional, context tag [3] = 0xa3)
576
+ let subjectAltName: string[] | undefined;
577
+ let extensions: DerValue[] | undefined;
578
+ for (let i = idx; i < tbsCertificate.children.length; i++) {
579
+ const child = tbsCertificate.children[i];
580
+ if (child.tag === 0xa3 && child.data.length > 0) {
581
+ // Extensions wrapper
582
+ const extSeq = parseDer(child.data);
583
+ if (extSeq.children) {
584
+ extensions = extSeq.children;
585
+ // Look for SAN extension (OID 2.5.29.17)
586
+ const SAN_OID = new Uint8Array([0x55, 0x1d, 0x11]);
587
+ for (const ext of extSeq.children) {
588
+ if (ext.children && ext.children.length >= 2) {
589
+ const extOid = ext.children[0];
590
+ if (extOid.tag === ASN1_OID && oidsEqual(extOid.data, SAN_OID)) {
591
+ const extValue = ext.children[ext.children.length - 1];
592
+ subjectAltName = parseSAN(extValue.data);
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ // Signature algorithm OID
601
+ let sigAlg = 'unknown';
602
+ if (signatureAlgorithm.children && signatureAlgorithm.children.length >= 1) {
603
+ sigAlg = oidToName(signatureAlgorithm.children[0].data);
604
+ }
605
+
606
+ // Signature value
607
+ const signature = signatureBitString.tag === ASN1_BIT_STRING
608
+ ? signatureBitString.data.slice(1) // skip unused-bits byte
609
+ : signatureBitString.data;
610
+
611
+ return {
612
+ raw: der,
613
+ tbsCertificate: tbsCertificate.data,
614
+ serialNumber,
615
+ issuer,
616
+ subject,
617
+ validFrom,
618
+ validTo,
619
+ publicKey,
620
+ publicKeyAlgorithm,
621
+ signatureAlgorithm: sigAlg,
622
+ signature,
623
+ subjectAltName,
624
+ extensions,
625
+ };
626
+ }
627
+
628
+ // ---- Distinguished Name (DN) parsing ----
629
+
630
+ const OID_NAMES: Record<string, string> = {
631
+ '2.5.4.3': 'CN',
632
+ '2.5.4.6': 'C',
633
+ '2.5.4.7': 'L',
634
+ '2.5.4.8': 'ST',
635
+ '2.5.4.10': 'O',
636
+ '2.5.4.11': 'OU',
637
+ '1.2.840.113549.1.9.1': 'emailAddress',
638
+ };
639
+
640
+ const SIG_ALG_NAMES: Record<string, string> = {
641
+ '1.2.840.113549.1.1.1': 'rsaEncryption',
642
+ '1.2.840.113549.1.1.5': 'sha1WithRSAEncryption',
643
+ '1.2.840.113549.1.1.11': 'sha256WithRSAEncryption',
644
+ '1.2.840.113549.1.1.12': 'sha384WithRSAEncryption',
645
+ '1.2.840.113549.1.1.13': 'sha512WithRSAEncryption',
646
+ };
647
+
648
+ function decodeOidString(data: Uint8Array): string {
649
+ if (data.length === 0) return '';
650
+ const components: number[] = [];
651
+ components.push(Math.floor(data[0] / 40));
652
+ components.push(data[0] % 40);
653
+ let value = 0;
654
+ for (let i = 1; i < data.length; i++) {
655
+ value = (value << 7) | (data[i] & 0x7f);
656
+ if (!(data[i] & 0x80)) {
657
+ components.push(value);
658
+ value = 0;
659
+ }
660
+ }
661
+ return components.join('.');
662
+ }
663
+
664
+ function oidToName(data: Uint8Array): string {
665
+ const oidStr = decodeOidString(data);
666
+ return SIG_ALG_NAMES[oidStr] || oidStr;
667
+ }
668
+
669
+ function parseDN(seq: DerValue): string {
670
+ if (!seq.children) return '';
671
+ const parts: string[] = [];
672
+ for (const rdn of seq.children) {
673
+ // Each RDN is a SET containing one or more SEQUENCE { OID, value }
674
+ const rdnChildren = rdn.tag === 0x31 ? parseSequenceChildren(rdn.data) : (rdn.children || []);
675
+ for (const atv of rdnChildren) {
676
+ if (atv.children && atv.children.length >= 2) {
677
+ const oidData = atv.children[0].data;
678
+ const oidStr = decodeOidString(oidData);
679
+ const name = OID_NAMES[oidStr] || oidStr;
680
+ const valueBytes = atv.children[1].data;
681
+ const value = new TextDecoder().decode(valueBytes);
682
+ parts.push(`${name}=${value}`);
683
+ }
684
+ }
685
+ }
686
+ return parts.join(', ');
687
+ }
688
+
689
+ function parseAsn1Time(tlv: DerValue): Date {
690
+ const str = new TextDecoder().decode(tlv.data);
691
+ if (tlv.tag === 0x17) {
692
+ // UTCTime: YYMMDDHHMMSSZ
693
+ let year = parseInt(str.substring(0, 2), 10);
694
+ year = year >= 50 ? 1900 + year : 2000 + year;
695
+ const month = parseInt(str.substring(2, 4), 10) - 1;
696
+ const day = parseInt(str.substring(4, 6), 10);
697
+ const hour = parseInt(str.substring(6, 8), 10);
698
+ const minute = parseInt(str.substring(8, 10), 10);
699
+ const second = parseInt(str.substring(10, 12), 10);
700
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
701
+ }
702
+ if (tlv.tag === 0x18) {
703
+ // GeneralizedTime: YYYYMMDDHHMMSSZ
704
+ const year = parseInt(str.substring(0, 4), 10);
705
+ const month = parseInt(str.substring(4, 6), 10) - 1;
706
+ const day = parseInt(str.substring(6, 8), 10);
707
+ const hour = parseInt(str.substring(8, 10), 10);
708
+ const minute = parseInt(str.substring(10, 12), 10);
709
+ const second = parseInt(str.substring(12, 14), 10);
710
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
711
+ }
712
+ throw new Error(`Unsupported time tag: 0x${tlv.tag.toString(16)}`);
713
+ }
714
+
715
+ function parseSAN(data: Uint8Array): string[] {
716
+ const names: string[] = [];
717
+ try {
718
+ const seq = parseDer(data);
719
+ if (seq.tag === ASN1_SEQUENCE && seq.children) {
720
+ for (const child of seq.children) {
721
+ // context-specific tags: [2] = dNSName, [7] = iPAddress
722
+ if (child.tag === 0x82) {
723
+ // dNSName
724
+ names.push('DNS:' + new TextDecoder().decode(child.data));
725
+ } else if (child.tag === 0x87) {
726
+ // iPAddress
727
+ if (child.data.length === 4) {
728
+ names.push('IP Address:' + child.data.join('.'));
729
+ } else if (child.data.length === 16) {
730
+ const parts: string[] = [];
731
+ for (let i = 0; i < 16; i += 2) {
732
+ parts.push(((child.data[i] << 8) | child.data[i + 1]).toString(16));
733
+ }
734
+ names.push('IP Address:' + parts.join(':'));
735
+ }
736
+ }
737
+ }
738
+ }
739
+ } catch {
740
+ // Parse error in SAN — return empty
741
+ }
742
+ return names;
743
+ }
744
+
745
+ // ============================================================================
746
+ // Public API
747
+ // ============================================================================
748
+
749
+ /**
750
+ * Parse a PEM-encoded RSA key. Supports:
751
+ * - RSA PUBLIC KEY (PKCS#1)
752
+ * - PUBLIC KEY (PKCS#8 SubjectPublicKeyInfo)
753
+ * - RSA PRIVATE KEY (PKCS#1)
754
+ * - PRIVATE KEY (PKCS#8 PrivateKeyInfo)
755
+ */
756
+ export function parsePemKey(pem: string): ParsedKey {
757
+ const { type, der } = pemToDer(pem);
758
+ const root = parseDer(der);
759
+
760
+ if (root.tag !== ASN1_SEQUENCE) {
761
+ throw new Error('Invalid key format: expected top-level SEQUENCE');
762
+ }
763
+
764
+ switch (type) {
765
+ case 'RSA PUBLIC KEY':
766
+ // PKCS#1 RSAPublicKey
767
+ return { type: 'rsa-public', components: parseRsaPublicKeyPkcs1(root) };
768
+
769
+ case 'PUBLIC KEY':
770
+ // PKCS#8 SubjectPublicKeyInfo
771
+ return { type: 'rsa-public', components: parseSubjectPublicKeyInfo(root) };
772
+
773
+ case 'RSA PRIVATE KEY':
774
+ // PKCS#1 RSAPrivateKey
775
+ return { type: 'rsa-private', components: parseRsaPrivateKeyPkcs1(root) };
776
+
777
+ case 'PRIVATE KEY':
778
+ // PKCS#8 PrivateKeyInfo
779
+ return { type: 'rsa-private', components: parsePrivateKeyInfo(root) };
780
+
781
+ default:
782
+ throw new Error(`Unsupported PEM type: ${type}`);
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Get the key size in bytes (byte length of modulus n).
788
+ */
789
+ export function rsaKeySize(n: bigint): number {
790
+ let bits = 0;
791
+ let val = n;
792
+ while (val > 0n) {
793
+ bits++;
794
+ val >>= 1n;
795
+ }
796
+ return Math.ceil(bits / 8);
797
+ }