@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.
- package/README.md +27 -0
- package/lib/esm/asn1.js +504 -0
- package/lib/esm/bigint-math.js +34 -0
- package/lib/esm/cipher.js +1272 -0
- package/lib/esm/constants.js +15 -0
- package/lib/esm/crypto-utils.js +47 -0
- package/lib/esm/dh.js +411 -0
- package/lib/esm/ecdh.js +356 -0
- package/lib/esm/ecdsa.js +125 -0
- package/lib/esm/hash.js +100 -0
- package/lib/esm/hkdf.js +58 -0
- package/lib/esm/hmac.js +93 -0
- package/lib/esm/index.js +158 -0
- package/lib/esm/key-object.js +330 -0
- package/lib/esm/mgf1.js +27 -0
- package/lib/esm/pbkdf2.js +68 -0
- package/lib/esm/public-encrypt.js +175 -0
- package/lib/esm/random.js +138 -0
- package/lib/esm/rsa-oaep.js +95 -0
- package/lib/esm/rsa-pss.js +100 -0
- package/lib/esm/scrypt.js +134 -0
- package/lib/esm/sign.js +248 -0
- package/lib/esm/timing-safe-equal.js +13 -0
- package/lib/esm/x509.js +214 -0
- package/lib/types/asn1.d.ts +87 -0
- package/lib/types/bigint-math.d.ts +13 -0
- package/lib/types/cipher.d.ts +84 -0
- package/lib/types/constants.d.ts +10 -0
- package/lib/types/crypto-utils.d.ts +22 -0
- package/lib/types/dh.d.ts +79 -0
- package/lib/types/ecdh.d.ts +96 -0
- package/lib/types/ecdsa.d.ts +21 -0
- package/lib/types/hash.d.ts +25 -0
- package/lib/types/hkdf.d.ts +9 -0
- package/lib/types/hmac.d.ts +20 -0
- package/lib/types/index.d.ts +105 -0
- package/lib/types/key-object.d.ts +36 -0
- package/lib/types/mgf1.d.ts +5 -0
- package/lib/types/pbkdf2.d.ts +9 -0
- package/lib/types/public-encrypt.d.ts +42 -0
- package/lib/types/random.d.ts +22 -0
- package/lib/types/rsa-oaep.d.ts +8 -0
- package/lib/types/rsa-pss.d.ts +8 -0
- package/lib/types/scrypt.d.ts +11 -0
- package/lib/types/sign.d.ts +61 -0
- package/lib/types/timing-safe-equal.d.ts +6 -0
- package/lib/types/x509.d.ts +72 -0
- package/package.json +45 -0
- package/src/asn1.ts +797 -0
- package/src/bigint-math.ts +45 -0
- package/src/cipher.spec.ts +332 -0
- package/src/cipher.ts +952 -0
- package/src/constants.ts +16 -0
- package/src/crypto-utils.ts +64 -0
- package/src/dh.spec.ts +111 -0
- package/src/dh.ts +761 -0
- package/src/ecdh.spec.ts +116 -0
- package/src/ecdh.ts +624 -0
- package/src/ecdsa.ts +243 -0
- package/src/extended.spec.ts +444 -0
- package/src/gcm.spec.ts +141 -0
- package/src/hash.spec.ts +86 -0
- package/src/hash.ts +119 -0
- package/src/hkdf.ts +99 -0
- package/src/hmac.spec.ts +64 -0
- package/src/hmac.ts +123 -0
- package/src/index.ts +93 -0
- package/src/key-object.spec.ts +202 -0
- package/src/key-object.ts +401 -0
- package/src/mgf1.ts +37 -0
- package/src/pbkdf2.spec.ts +76 -0
- package/src/pbkdf2.ts +106 -0
- package/src/public-encrypt.ts +288 -0
- package/src/random.spec.ts +133 -0
- package/src/random.ts +183 -0
- package/src/rsa-oaep.ts +167 -0
- package/src/rsa-pss.ts +190 -0
- package/src/scrypt.spec.ts +90 -0
- package/src/scrypt.ts +191 -0
- package/src/sign.spec.ts +160 -0
- package/src/sign.ts +319 -0
- package/src/test.mts +19 -0
- package/src/timing-safe-equal.ts +21 -0
- package/src/x509.spec.ts +210 -0
- package/src/x509.ts +262 -0
- package/tsconfig.json +31 -0
- 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
|
+
}
|