@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/scrypt.ts ADDED
@@ -0,0 +1,191 @@
1
+ // Implements scrypt per RFC 7914 (The scrypt Password-Based Key Derivation Function)
2
+ // Reference: refs/node/lib/internal/crypto/scrypt.js
3
+ // Copyright (c) Node.js contributors. MIT license.
4
+ // Modifications: Pure-JS implementation for GJS using internal PBKDF2/Hmac
5
+
6
+ import { Buffer } from 'node:buffer';
7
+ import { pbkdf2Sync } from './pbkdf2.js';
8
+
9
+ export interface ScryptOptions {
10
+ N?: number; // CPU/memory cost parameter (default: 16384)
11
+ r?: number; // Block size parameter (default: 8)
12
+ p?: number; // Parallelization parameter (default: 1)
13
+ maxmem?: number;
14
+ }
15
+
16
+ type ScryptCallback = (err: Error | null, derivedKey: Buffer) => void;
17
+
18
+ // ---- Salsa20/8 core ----
19
+
20
+ function R(a: number, b: number): number {
21
+ return ((a << b) | (a >>> (32 - b))) >>> 0;
22
+ }
23
+
24
+ function salsa20_8(B: Uint32Array): void {
25
+ const x = new Uint32Array(16);
26
+ for (let i = 0; i < 16; i++) x[i] = B[i];
27
+
28
+ for (let i = 0; i < 4; i++) {
29
+ // Column round
30
+ x[ 4] ^= R(x[ 0]+x[12], 7); x[ 8] ^= R(x[ 4]+x[ 0], 9);
31
+ x[12] ^= R(x[ 8]+x[ 4],13); x[ 0] ^= R(x[12]+x[ 8],18);
32
+ x[ 9] ^= R(x[ 5]+x[ 1], 7); x[13] ^= R(x[ 9]+x[ 5], 9);
33
+ x[ 1] ^= R(x[13]+x[ 9],13); x[ 5] ^= R(x[ 1]+x[13],18);
34
+ x[14] ^= R(x[10]+x[ 6], 7); x[ 2] ^= R(x[14]+x[10], 9);
35
+ x[ 6] ^= R(x[ 2]+x[14],13); x[10] ^= R(x[ 6]+x[ 2],18);
36
+ x[ 3] ^= R(x[15]+x[11], 7); x[ 7] ^= R(x[ 3]+x[15], 9);
37
+ x[11] ^= R(x[ 7]+x[ 3],13); x[15] ^= R(x[11]+x[ 7],18);
38
+ // Row round
39
+ x[ 1] ^= R(x[ 0]+x[ 3], 7); x[ 2] ^= R(x[ 1]+x[ 0], 9);
40
+ x[ 3] ^= R(x[ 2]+x[ 1],13); x[ 0] ^= R(x[ 3]+x[ 2],18);
41
+ x[ 6] ^= R(x[ 5]+x[ 4], 7); x[ 7] ^= R(x[ 6]+x[ 5], 9);
42
+ x[ 4] ^= R(x[ 7]+x[ 6],13); x[ 5] ^= R(x[ 4]+x[ 7],18);
43
+ x[11] ^= R(x[10]+x[ 9], 7); x[ 8] ^= R(x[11]+x[10], 9);
44
+ x[ 9] ^= R(x[ 8]+x[11],13); x[10] ^= R(x[ 9]+x[ 8],18);
45
+ x[12] ^= R(x[15]+x[14], 7); x[13] ^= R(x[12]+x[15], 9);
46
+ x[14] ^= R(x[13]+x[12],13); x[15] ^= R(x[14]+x[13],18);
47
+ }
48
+
49
+ for (let i = 0; i < 16; i++) B[i] = (B[i] + x[i]) >>> 0;
50
+ }
51
+
52
+ // ---- BlockMix (Salsa20/8) ----
53
+
54
+ function blockMix(B: Uint32Array, r: number): void {
55
+ const blockWords = 2 * r * 16;
56
+ const X = new Uint32Array(16);
57
+ // X = B[2r-1]
58
+ for (let i = 0; i < 16; i++) X[i] = B[blockWords - 16 + i];
59
+
60
+ const Y = new Uint32Array(blockWords);
61
+
62
+ for (let i = 0; i < 2 * r; i++) {
63
+ // X = X ⊕ B[i]
64
+ for (let j = 0; j < 16; j++) X[j] ^= B[i * 16 + j];
65
+ salsa20_8(X);
66
+ // Y[i] = X
67
+ for (let j = 0; j < 16; j++) Y[i * 16 + j] = X[j];
68
+ }
69
+
70
+ // B = [Y[0], Y[2], ..., Y[2r-2], Y[1], Y[3], ..., Y[2r-1]]
71
+ for (let i = 0; i < r; i++) {
72
+ for (let j = 0; j < 16; j++) B[i * 16 + j] = Y[2 * i * 16 + j];
73
+ }
74
+ for (let i = 0; i < r; i++) {
75
+ for (let j = 0; j < 16; j++) B[(r + i) * 16 + j] = Y[(2 * i + 1) * 16 + j];
76
+ }
77
+ }
78
+
79
+ // ---- ROMix ----
80
+
81
+ function roMix(B: Uint32Array, N: number, r: number): void {
82
+ const blockWords = 2 * r * 16;
83
+ const V = new Array<Uint32Array>(N);
84
+
85
+ for (let i = 0; i < N; i++) {
86
+ V[i] = new Uint32Array(B);
87
+ blockMix(B, r);
88
+ }
89
+
90
+ for (let i = 0; i < N; i++) {
91
+ // j = Integerify(B) mod N
92
+ const j = B[blockWords - 16] & (N - 1);
93
+ for (let k = 0; k < blockWords; k++) B[k] ^= V[j][k];
94
+ blockMix(B, r);
95
+ }
96
+ }
97
+
98
+ // ---- Bytes to Uint32Array (little-endian) ----
99
+
100
+ function bytesToWords(bytes: Uint8Array): Uint32Array {
101
+ const words = new Uint32Array(bytes.length / 4);
102
+ for (let i = 0; i < words.length; i++) {
103
+ words[i] = bytes[i*4] | (bytes[i*4+1] << 8) | (bytes[i*4+2] << 16) | (bytes[i*4+3] << 24);
104
+ }
105
+ return words;
106
+ }
107
+
108
+ function wordsToBytes(words: Uint32Array): Uint8Array {
109
+ const bytes = new Uint8Array(words.length * 4);
110
+ for (let i = 0; i < words.length; i++) {
111
+ bytes[i*4] = words[i] & 0xff;
112
+ bytes[i*4+1] = (words[i] >> 8) & 0xff;
113
+ bytes[i*4+2] = (words[i] >> 16) & 0xff;
114
+ bytes[i*4+3] = (words[i] >> 24) & 0xff;
115
+ }
116
+ return bytes;
117
+ }
118
+
119
+ // ---- scrypt core ----
120
+
121
+ function scryptCore(
122
+ password: Buffer | Uint8Array,
123
+ salt: Buffer | Uint8Array,
124
+ N: number,
125
+ r: number,
126
+ p: number,
127
+ keyLen: number,
128
+ ): Buffer {
129
+ // Step 1: Generate initial blocks with PBKDF2-SHA256
130
+ const blockSize = 128 * r;
131
+ const B = pbkdf2Sync(password, salt, 1, p * blockSize, 'sha256');
132
+
133
+ // Step 2: Apply ROMix to each block
134
+ for (let i = 0; i < p; i++) {
135
+ const blockBytes = new Uint8Array(B.buffer, B.byteOffset + i * blockSize, blockSize);
136
+ const blockWords = bytesToWords(blockBytes);
137
+ roMix(blockWords, N, r);
138
+ const result = wordsToBytes(blockWords);
139
+ blockBytes.set(result);
140
+ }
141
+
142
+ // Step 3: Derive final key with PBKDF2-SHA256
143
+ return pbkdf2Sync(password, B, 1, keyLen, 'sha256');
144
+ }
145
+
146
+ // ---- Public API ----
147
+
148
+ export function scryptSync(
149
+ password: string | Buffer | Uint8Array,
150
+ salt: string | Buffer | Uint8Array,
151
+ keylen: number,
152
+ options?: ScryptOptions,
153
+ ): Buffer {
154
+ const pwd = typeof password === 'string' ? Buffer.from(password) : Buffer.from(password);
155
+ const slt = typeof salt === 'string' ? Buffer.from(salt) : Buffer.from(salt);
156
+ const N = options?.N ?? 16384;
157
+ const r = options?.r ?? 8;
158
+ const p = options?.p ?? 1;
159
+
160
+ if (N <= 0 || (N & (N - 1)) !== 0) {
161
+ throw new Error('N must be a positive power of 2');
162
+ }
163
+
164
+ return scryptCore(pwd, slt, N, r, p, keylen);
165
+ }
166
+
167
+ export function scrypt(
168
+ password: string | Buffer | Uint8Array,
169
+ salt: string | Buffer | Uint8Array,
170
+ keylen: number,
171
+ optionsOrCallback: ScryptOptions | ScryptCallback,
172
+ callback?: ScryptCallback,
173
+ ): void {
174
+ let options: ScryptOptions = {};
175
+ let cb: ScryptCallback;
176
+
177
+ if (typeof optionsOrCallback === 'function') {
178
+ cb = optionsOrCallback;
179
+ } else {
180
+ options = optionsOrCallback;
181
+ cb = callback!;
182
+ }
183
+
184
+ try {
185
+ const result = scryptSync(password, salt, keylen, options);
186
+ // Use setTimeout to make it async
187
+ setTimeout(() => cb(null, result), 0);
188
+ } catch (err) {
189
+ setTimeout(() => cb(err instanceof Error ? err : new Error(String(err)), Buffer.alloc(0)), 0);
190
+ }
191
+ }
@@ -0,0 +1,160 @@
1
+ // Ported from refs/node-test/parallel/test-crypto-sign-verify.js
2
+ // Original: MIT license, Copyright (c) Node.js contributors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import { createSign, createVerify, publicEncrypt, privateDecrypt } from 'node:crypto';
6
+ import { Buffer } from 'node:buffer';
7
+
8
+ // 2048-bit RSA key pair in PKCS#8/SPKI format for cross-platform compatibility
9
+ const RSA_PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
10
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVJethEusbgt7c
11
+ sPDaTGP+gKEdQhCSVM3Lyc4RSD1gfxLjU14zONlAeR1szp5PEWUQ2SCBykAcDa4t
12
+ wy/pYCx1TXoHozLb9XKPqMKEBgWWiXh0d1Lwl2RYZViw+veBlTc3yc60Bmv5vb75
13
+ S203jBAu5NbBlPbiMtiCffYXsppTVqyaDYuPUTZCga/mJUgPFupi/e5yyqqpYo44
14
+ JyCSskGJ/0c7+cvsQaaF5zlbB4SJY5oFqaDAQIpY/I26mU8+fUY8CiDA3WFc+EzD
15
+ 2d7mFyhp9J83hVFBRP+LGx+MQ+ZHQanWECNHfe3+K08tlU88cnCGzZuX1gPPFDdH
16
+ G6ugIelxAgMBAAECggEAYx8QOAOBPDj/BOhwCUSPF9KfmiiX5kTzszp0zwqmKFLP
17
+ 6NFjNDTSqy3npirr6d8v/cbLXDA+4gzmnDdx93iXFDHkdtrJEwswrGgRlS3ruVbS
18
+ om6/Lk1pB8aRmTQMl8FZfWMm8gcufWRlBC+0aamD+RrIWBu7N/PnRb/oCpsvM2N4
19
+ +ZYxT3OJ21egUEF5WecVf5IyXJM8MWhfugtDnh24XtJAktXMNW4oQoBXyjgopTv6
20
+ Gcx+vkbpDkPSWWGpI8ztt4CB2U17eWAGx6S72DECkJurmBpJW59cerAKYMpcsrwY
21
+ eZ0NSNb1MGBe/Q3EFIg71ll7kjlKRQf1qbWEdhEA5QKBgQDrvxiqgB3Rfrn/1ma4
22
+ F0iY4CiguUVdEK9OiDmfkWlIzvhpxiQJOBOoPzb+3taUmnAdaB2UYyHk5d4yBm46
23
+ zGAQjVYagWN2jDy0E2QmQWDioT7gLXCQvHVDYXVt9u+L9orimOGoKF3ph6lxKtT4
24
+ C/elC9sulWEwITIjwvwuK6lR8wKBgQDndc4W/1Frk/bluonw6kDhLOOmp7D+B0Uk
25
+ tCCx7BB6oOpCsZRZ47X/JOhnWh0bQ7mPIWKMgRcox1pCoPU5efvvD9hCEqCGRW9i
26
+ S4yMOdwS2doXI12KZlkLmVWvbQ4EcYzpeprOKaXgQ/YMxMvwhlkTNZ71OQw0GPp0
27
+ eBfPrOwMCwKBgDLVvkfl4IgwP4N/hB7mRm1QyPH/gYmT83mHvoU+IenlV4PXiiXC
28
+ xdpd50oGW1coBk0RCm/ZAJIPT16SLGrZb02ibJLCm+QQUXazR8FID9BO3PQSWFed
29
+ i9u/xEa2HOmdfE1okiBks/uLmWohxlLGodwhNl5RL+flAJ7diOub1qMpAoGBAMn8
30
+ 3GTlWsBu17+TEl3Tj9rxuZjuLl8BKS3mo8GhKKBbXRPmtHfdaC3In6fR1CS+7Wgi
31
+ 0kWbQgKsNfB/VoFaGql9QlQmvT9vyMwW8ghNVeh9hP08N51Xw82Demsk2F64WShH
32
+ fmD7p24W4Nozw2WbWJCS8q09o5CzW53YT69EUJoRAoGAQ8IoK50pzLpWD5HBHpqU
33
+ /El/jaozqwctQAv5nREqG9BBHaRcCpKfPAfINF31Xy1CssYe8W8xPNkZyrNPYVC/
34
+ Yi7MeKZhG0BYTXq8CW0O1x8P2fA2KfxmvCPIEKn1QudJw4gAbDpnsKJD14hQtGYb
35
+ CJbXIFhqfWsYSVJJwIK9uL4=
36
+ -----END PRIVATE KEY-----`;
37
+
38
+ const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
39
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1SXrYRLrG4Le3LDw2kxj
40
+ /oChHUIQklTNy8nOEUg9YH8S41NeMzjZQHkdbM6eTxFlENkggcpAHA2uLcMv6WAs
41
+ dU16B6My2/Vyj6jChAYFlol4dHdS8JdkWGVYsPr3gZU3N8nOtAZr+b2++UttN4wQ
42
+ LuTWwZT24jLYgn32F7KaU1asmg2Lj1E2QoGv5iVIDxbqYv3ucsqqqWKOOCcgkrJB
43
+ if9HO/nL7EGmhec5WweEiWOaBamgwECKWPyNuplPPn1GPAogwN1hXPhMw9ne5hco
44
+ afSfN4VRQUT/ixsfjEPmR0Gp1hAjR33t/itPLZVPPHJwhs2bl9YDzxQ3RxuroCHp
45
+ cQIDAQAB
46
+ -----END PUBLIC KEY-----`;
47
+
48
+ export default async () => {
49
+
50
+ await describe('crypto.createSign', async () => {
51
+ await it('should be a function', async () => {
52
+ expect(typeof createSign).toBe('function');
53
+ });
54
+
55
+ await it('should create a Sign instance', async () => {
56
+ const sign = createSign('SHA256');
57
+ expect(sign).toBeDefined();
58
+ expect(typeof sign.update).toBe('function');
59
+ expect(typeof sign.sign).toBe('function');
60
+ });
61
+
62
+ await it('should sign data with RSA-SHA256', async () => {
63
+ const sign = createSign('SHA256');
64
+ sign.update('Hello, World!');
65
+ const signature = sign.sign(RSA_PRIVATE_KEY);
66
+ expect(Buffer.isBuffer(signature)).toBe(true);
67
+ expect(signature.length).toBeGreaterThan(0);
68
+ });
69
+
70
+ await it('should produce different signatures for different data', async () => {
71
+ const sign1 = createSign('SHA256');
72
+ sign1.update('data1');
73
+ const sig1 = sign1.sign(RSA_PRIVATE_KEY);
74
+
75
+ const sign2 = createSign('SHA256');
76
+ sign2.update('data2');
77
+ const sig2 = sign2.sign(RSA_PRIVATE_KEY);
78
+
79
+ expect(sig1.toString('hex')).not.toBe(sig2.toString('hex'));
80
+ });
81
+ });
82
+
83
+ await describe('crypto.createVerify', async () => {
84
+ await it('should be a function', async () => {
85
+ expect(typeof createVerify).toBe('function');
86
+ });
87
+
88
+ await it('should verify a valid signature', async () => {
89
+ const data = 'test data for signing';
90
+
91
+ const sign = createSign('SHA256');
92
+ sign.update(data);
93
+ const signature = sign.sign(RSA_PRIVATE_KEY);
94
+
95
+ const verify = createVerify('SHA256');
96
+ verify.update(data);
97
+ const isValid = verify.verify(RSA_PUBLIC_KEY, signature);
98
+ expect(isValid).toBe(true);
99
+ });
100
+
101
+ await it('should reject invalid signature', async () => {
102
+ const sign = createSign('SHA256');
103
+ sign.update('original data');
104
+ const signature = sign.sign(RSA_PRIVATE_KEY);
105
+
106
+ const verify = createVerify('SHA256');
107
+ verify.update('tampered data');
108
+ const isValid = verify.verify(RSA_PUBLIC_KEY, signature);
109
+ expect(isValid).toBe(false);
110
+ });
111
+
112
+ await it('should support multiple update calls', async () => {
113
+ const sign = createSign('SHA256');
114
+ sign.update('part1');
115
+ sign.update('part2');
116
+ const signature = sign.sign(RSA_PRIVATE_KEY);
117
+
118
+ const verify = createVerify('SHA256');
119
+ verify.update('part1');
120
+ verify.update('part2');
121
+ expect(verify.verify(RSA_PUBLIC_KEY, signature)).toBe(true);
122
+ });
123
+
124
+ await it('should support hex encoding for signature', async () => {
125
+ const sign = createSign('SHA256');
126
+ sign.update('test');
127
+ const sigHex = sign.sign(RSA_PRIVATE_KEY, 'hex');
128
+ expect(typeof sigHex).toBe('string');
129
+
130
+ const verify = createVerify('SHA256');
131
+ verify.update('test');
132
+ expect(verify.verify(RSA_PUBLIC_KEY, sigHex, 'hex')).toBe(true);
133
+ });
134
+ });
135
+
136
+ await describe('crypto.publicEncrypt / privateDecrypt', async () => {
137
+ await it('should be functions', async () => {
138
+ expect(typeof publicEncrypt).toBe('function');
139
+ expect(typeof privateDecrypt).toBe('function');
140
+ });
141
+
142
+ await it('should encrypt and decrypt round-trip', async () => {
143
+ const plaintext = Buffer.from('secret message');
144
+ const encrypted = publicEncrypt(RSA_PUBLIC_KEY, plaintext);
145
+ expect(Buffer.isBuffer(encrypted)).toBe(true);
146
+ expect(encrypted.length).toBeGreaterThan(0);
147
+
148
+ const decrypted = privateDecrypt(RSA_PRIVATE_KEY, encrypted);
149
+ expect(decrypted.toString()).toBe('secret message');
150
+ });
151
+
152
+ await it('should produce different ciphertext each time (PKCS#1 random padding)', async () => {
153
+ const plaintext = Buffer.from('test');
154
+ const enc1 = publicEncrypt(RSA_PUBLIC_KEY, plaintext);
155
+ const enc2 = publicEncrypt(RSA_PUBLIC_KEY, plaintext);
156
+ // Due to random padding, ciphertexts should differ
157
+ expect(enc1.toString('hex')).not.toBe(enc2.toString('hex'));
158
+ });
159
+ });
160
+ };
package/src/sign.ts ADDED
@@ -0,0 +1,319 @@
1
+ // Sign/Verify — RSA PKCS#1 v1.5 signature scheme for GJS
2
+ // Reference: refs/browserify-sign/browser/sign.js, refs/browserify-sign/browser/verify.js
3
+ // Reimplemented for GJS using native BigInt (ES2024)
4
+
5
+ import { Buffer } from 'node:buffer';
6
+ import { Hash } from './hash.js';
7
+ import { parsePemKey, rsaKeySize } from './asn1.js';
8
+ import type { RsaPrivateComponents, RsaPublicComponents } from './asn1.js';
9
+ import { modPow, bigIntToBytes, bytesToBigInt } from './bigint-math.js';
10
+
11
+ // ============================================================================
12
+ // PKCS#1 v1.5 DigestInfo structures
13
+ // ============================================================================
14
+
15
+ /**
16
+ * DigestInfo DER prefix bytes for each supported hash algorithm.
17
+ * These encode: SEQUENCE { SEQUENCE { OID hashAlg, NULL }, OCTET STRING hashValue }
18
+ * excluding the actual hash value at the end.
19
+ */
20
+ const DIGEST_INFO_PREFIX: Record<string, Uint8Array> = {
21
+ sha1: new Uint8Array([
22
+ 0x30, 0x21, 0x30, 0x09, 0x06, 0x05,
23
+ 0x2b, 0x0e, 0x03, 0x02, 0x1a,
24
+ 0x05, 0x00, 0x04, 0x14,
25
+ ]),
26
+ sha256: new Uint8Array([
27
+ 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09,
28
+ 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
29
+ 0x05, 0x00, 0x04, 0x20,
30
+ ]),
31
+ sha512: new Uint8Array([
32
+ 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09,
33
+ 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03,
34
+ 0x05, 0x00, 0x04, 0x40,
35
+ ]),
36
+ };
37
+
38
+ // ============================================================================
39
+ // Algorithm normalization
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Normalize algorithm strings like "RSA-SHA256", "SHA256", "sha256" to
44
+ * the canonical hash name used internally (e.g. "sha256").
45
+ */
46
+ function normalizeSignAlgorithm(algorithm: string): string {
47
+ let alg = algorithm.toLowerCase().replace(/-/g, '');
48
+ // Strip leading "rsa" prefix (e.g., "rsasha256" -> "sha256")
49
+ if (alg.startsWith('rsa')) {
50
+ alg = alg.slice(3);
51
+ }
52
+ if (!DIGEST_INFO_PREFIX[alg]) {
53
+ throw new Error(`Unsupported algorithm: ${algorithm}. Supported: RSA-SHA1, RSA-SHA256, RSA-SHA512`);
54
+ }
55
+ return alg;
56
+ }
57
+
58
+ // ============================================================================
59
+ // Key extraction helpers
60
+ // ============================================================================
61
+
62
+ interface KeyInput {
63
+ key: string;
64
+ passphrase?: string;
65
+ padding?: number;
66
+ }
67
+
68
+ function extractPem(key: string | Buffer | KeyInput): string {
69
+ if (typeof key === 'string') {
70
+ return key;
71
+ }
72
+ if (Buffer.isBuffer(key) || key instanceof Uint8Array) {
73
+ return Buffer.from(key).toString('utf8');
74
+ }
75
+ if (key && typeof key === 'object' && 'key' in key) {
76
+ const k = key.key;
77
+ if (typeof k === 'string') return k;
78
+ if (Buffer.isBuffer(k) || (k as unknown) instanceof Uint8Array) return Buffer.from(k as Uint8Array).toString('utf8');
79
+ }
80
+ throw new TypeError('Invalid key argument');
81
+ }
82
+
83
+ // ============================================================================
84
+ // Sign class
85
+ // ============================================================================
86
+
87
+ /**
88
+ * The Sign class generates RSA PKCS#1 v1.5 signatures.
89
+ *
90
+ * Usage:
91
+ * const sign = createSign('RSA-SHA256');
92
+ * sign.update('data');
93
+ * const signature = sign.sign(privateKey);
94
+ */
95
+ export class Sign {
96
+ private _algorithm: string;
97
+ private _hash: Hash;
98
+ private _finalized = false;
99
+
100
+ constructor(algorithm: string) {
101
+ this._algorithm = normalizeSignAlgorithm(algorithm);
102
+ this._hash = new Hash(this._algorithm);
103
+ }
104
+
105
+ /**
106
+ * Update the Sign object with the given data.
107
+ */
108
+ update(data: string | Buffer | Uint8Array, inputEncoding?: BufferEncoding): this {
109
+ if (this._finalized) {
110
+ throw new Error('Sign was already finalized');
111
+ }
112
+ this._hash.update(data, inputEncoding);
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Compute the signature using the private key.
118
+ * Returns the signature as a Buffer (or string if outputEncoding is given).
119
+ */
120
+ sign(privateKey: string | Buffer | KeyInput, outputEncoding?: BufferEncoding): Buffer | string {
121
+ if (this._finalized) {
122
+ throw new Error('Sign was already finalized');
123
+ }
124
+ this._finalized = true;
125
+
126
+ // Hash the accumulated data
127
+ const digest = this._hash.digest() as Buffer;
128
+
129
+ // Parse the private key
130
+ const pem = extractPem(privateKey);
131
+ const parsed = parsePemKey(pem);
132
+ if (parsed.type !== 'rsa-private') {
133
+ throw new Error('privateKey must be an RSA private key');
134
+ }
135
+ const { n, e, d } = parsed.components;
136
+ const keyLen = rsaKeySize(n);
137
+
138
+ // Build DigestInfo = prefix || hash
139
+ const prefix = DIGEST_INFO_PREFIX[this._algorithm];
140
+ const digestInfo = new Uint8Array(prefix.length + digest.length);
141
+ digestInfo.set(prefix, 0);
142
+ digestInfo.set(digest, prefix.length);
143
+
144
+ // Apply PKCS#1 v1.5 Type 1 padding
145
+ // 0x00 0x01 [0xFF padding] 0x00 [DigestInfo]
146
+ const padLen = keyLen - digestInfo.length - 3;
147
+ if (padLen < 8) {
148
+ throw new Error('Key is too short for the specified hash algorithm');
149
+ }
150
+
151
+ const em = new Uint8Array(keyLen);
152
+ em[0] = 0x00;
153
+ em[1] = 0x01;
154
+ for (let i = 2; i < 2 + padLen; i++) {
155
+ em[i] = 0xff;
156
+ }
157
+ em[2 + padLen] = 0x00;
158
+ em.set(digestInfo, 3 + padLen);
159
+
160
+ // RSA private key operation: signature = em^d mod n
161
+ const m = bytesToBigInt(em);
162
+ const s = modPow(m, d, n);
163
+ const sigBytes = bigIntToBytes(s, keyLen);
164
+ const sigBuf = Buffer.from(sigBytes);
165
+
166
+ if (outputEncoding) {
167
+ return sigBuf.toString(outputEncoding);
168
+ }
169
+ return sigBuf;
170
+ }
171
+ }
172
+
173
+ // ============================================================================
174
+ // Verify class
175
+ // ============================================================================
176
+
177
+ /**
178
+ * The Verify class verifies RSA PKCS#1 v1.5 signatures.
179
+ *
180
+ * Usage:
181
+ * const verify = createVerify('RSA-SHA256');
182
+ * verify.update('data');
183
+ * const ok = verify.verify(publicKey, signature);
184
+ */
185
+ export class Verify {
186
+ private _algorithm: string;
187
+ private _hash: Hash;
188
+ private _finalized = false;
189
+
190
+ constructor(algorithm: string) {
191
+ this._algorithm = normalizeSignAlgorithm(algorithm);
192
+ this._hash = new Hash(this._algorithm);
193
+ }
194
+
195
+ /**
196
+ * Update the Verify object with the given data.
197
+ */
198
+ update(data: string | Buffer | Uint8Array, inputEncoding?: BufferEncoding): this {
199
+ if (this._finalized) {
200
+ throw new Error('Verify was already finalized');
201
+ }
202
+ this._hash.update(data, inputEncoding);
203
+ return this;
204
+ }
205
+
206
+ /**
207
+ * Verify the signature against the public key.
208
+ * Returns true if the signature is valid, false otherwise.
209
+ */
210
+ verify(
211
+ publicKey: string | Buffer | KeyInput,
212
+ signature: string | Buffer | Uint8Array,
213
+ signatureEncoding?: BufferEncoding,
214
+ ): boolean {
215
+ if (this._finalized) {
216
+ throw new Error('Verify was already finalized');
217
+ }
218
+ this._finalized = true;
219
+
220
+ // Hash the accumulated data
221
+ const digest = this._hash.digest() as Buffer;
222
+
223
+ // Parse the public key
224
+ const pem = extractPem(publicKey);
225
+ const parsed = parsePemKey(pem);
226
+
227
+ let n: bigint;
228
+ let e: bigint;
229
+ if (parsed.type === 'rsa-public') {
230
+ n = parsed.components.n;
231
+ e = parsed.components.e;
232
+ } else if (parsed.type === 'rsa-private') {
233
+ // Allow using a private key for verification (extract public components)
234
+ n = parsed.components.n;
235
+ e = parsed.components.e;
236
+ } else {
237
+ throw new Error('publicKey must be an RSA public or private key');
238
+ }
239
+
240
+ const keyLen = rsaKeySize(n);
241
+
242
+ // Decode the signature
243
+ let sigBytes: Uint8Array;
244
+ if (typeof signature === 'string') {
245
+ sigBytes = Buffer.from(signature, signatureEncoding || 'base64');
246
+ } else {
247
+ sigBytes = signature instanceof Uint8Array ? signature : Buffer.from(signature);
248
+ }
249
+
250
+ if (sigBytes.length !== keyLen) {
251
+ return false;
252
+ }
253
+
254
+ // RSA public key operation: em = signature^e mod n
255
+ const s = bytesToBigInt(sigBytes);
256
+ if (s >= n) {
257
+ return false;
258
+ }
259
+ const m = modPow(s, e, n);
260
+ const em = bigIntToBytes(m, keyLen);
261
+
262
+ // Verify PKCS#1 v1.5 Type 1 padding structure
263
+ // Expected: 0x00 0x01 [0xFF...] 0x00 [DigestInfo]
264
+ if (em[0] !== 0x00 || em[1] !== 0x01) {
265
+ return false;
266
+ }
267
+
268
+ // Find the 0x00 separator after the 0xFF padding
269
+ let sepIdx = 2;
270
+ while (sepIdx < em.length && em[sepIdx] === 0xff) {
271
+ sepIdx++;
272
+ }
273
+ if (sepIdx >= em.length || em[sepIdx] !== 0x00) {
274
+ return false;
275
+ }
276
+ // Must have at least 8 bytes of 0xFF padding
277
+ if (sepIdx - 2 < 8) {
278
+ return false;
279
+ }
280
+ sepIdx++; // skip the 0x00 separator
281
+
282
+ // Extract DigestInfo from the decrypted message
283
+ const recoveredDigestInfo = em.slice(sepIdx);
284
+
285
+ // Build expected DigestInfo
286
+ const prefix = DIGEST_INFO_PREFIX[this._algorithm];
287
+ const expectedDigestInfo = new Uint8Array(prefix.length + digest.length);
288
+ expectedDigestInfo.set(prefix, 0);
289
+ expectedDigestInfo.set(digest, prefix.length);
290
+
291
+ // Constant-time comparison
292
+ if (recoveredDigestInfo.length !== expectedDigestInfo.length) {
293
+ return false;
294
+ }
295
+ let diff = 0;
296
+ for (let i = 0; i < recoveredDigestInfo.length; i++) {
297
+ diff |= recoveredDigestInfo[i] ^ expectedDigestInfo[i];
298
+ }
299
+ return diff === 0;
300
+ }
301
+ }
302
+
303
+ // ============================================================================
304
+ // Factory functions
305
+ // ============================================================================
306
+
307
+ /**
308
+ * Create and return a Sign object for the given algorithm.
309
+ */
310
+ export function createSign(algorithm: string): Sign {
311
+ return new Sign(algorithm);
312
+ }
313
+
314
+ /**
315
+ * Create and return a Verify object for the given algorithm.
316
+ */
317
+ export function createVerify(algorithm: string): Verify {
318
+ return new Verify(algorithm);
319
+ }
package/src/test.mts ADDED
@@ -0,0 +1,19 @@
1
+
2
+ import { run } from '@gjsify/unit';
3
+
4
+ import testSuiteHash from './hash.spec.js';
5
+ import testSuiteHmac from './hmac.spec.js';
6
+ import testSuiteRandom from './random.spec.js';
7
+ import testSuitePbkdf2 from './pbkdf2.spec.js';
8
+ import testSuiteCipher from './cipher.spec.js';
9
+ import testSuiteScrypt from './scrypt.spec.js';
10
+ import testSuiteDh from './dh.spec.js';
11
+ import testSuiteEcdh from './ecdh.spec.js';
12
+ import testSuiteGcm from './gcm.spec.js';
13
+ import testSuiteSign from './sign.spec.js';
14
+ import testSuiteKeyObject from './key-object.spec.js';
15
+ import testSuiteX509 from './x509.spec.js';
16
+
17
+ import testSuiteExtended from './extended.spec.js';
18
+
19
+ run({ testSuiteHash, testSuiteHmac, testSuiteRandom, testSuitePbkdf2, testSuiteCipher, testSuiteScrypt, testSuiteDh, testSuiteEcdh, testSuiteGcm, testSuiteSign, testSuiteKeyObject, testSuiteX509, testSuiteExtended });