@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/mgf1.ts ADDED
@@ -0,0 +1,37 @@
1
+ // MGF1 (Mask Generation Function 1) per RFC 8017 Section B.2.1
2
+ // Used by RSA-PSS and RSA-OAEP
3
+
4
+ import { Hash } from './hash.js';
5
+ import { hashSize } from './crypto-utils.js';
6
+
7
+ /**
8
+ * MGF1 mask generation function.
9
+ * Produces a mask of `length` bytes from `seed` using `hashAlgo`.
10
+ */
11
+ export function mgf1(hashAlgo: string, seed: Uint8Array, length: number): Uint8Array {
12
+ const hashLen = hashSize(hashAlgo);
13
+ const mask = new Uint8Array(length);
14
+ let offset = 0;
15
+ let counter = 0;
16
+
17
+ while (offset < length) {
18
+ // counter as 4-byte big-endian
19
+ const C = new Uint8Array(4);
20
+ C[0] = (counter >>> 24) & 0xff;
21
+ C[1] = (counter >>> 16) & 0xff;
22
+ C[2] = (counter >>> 8) & 0xff;
23
+ C[3] = counter & 0xff;
24
+
25
+ const hash = new Hash(hashAlgo);
26
+ hash.update(seed);
27
+ hash.update(C);
28
+ const digest = new Uint8Array(hash.digest() as any);
29
+
30
+ const toCopy = Math.min(digest.length, length - offset);
31
+ mask.set(digest.slice(0, toCopy), offset);
32
+ offset += toCopy;
33
+ counter++;
34
+ }
35
+
36
+ return mask;
37
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { pbkdf2, pbkdf2Sync } from 'node:crypto';
3
+ import { Buffer } from 'node:buffer';
4
+
5
+ export default async () => {
6
+ await describe('crypto.pbkdf2Sync', async () => {
7
+ await it('should be a function', async () => {
8
+ expect(typeof pbkdf2Sync).toBe('function');
9
+ });
10
+
11
+ await it('should derive key with sha256', async () => {
12
+ const key = pbkdf2Sync('password', 'salt', 1000, 32, 'sha256');
13
+ expect(key).toBeDefined();
14
+ expect(key.length).toBe(32);
15
+ });
16
+
17
+ await it('should derive key with sha1 (default)', async () => {
18
+ const key = pbkdf2Sync('password', 'salt', 1, 20, 'sha1');
19
+ expect(key).toBeDefined();
20
+ expect(key.length).toBe(20);
21
+ // Known test vector: PBKDF2-HMAC-SHA1("password", "salt", 1, 20)
22
+ expect(key.toString('hex')).toBe('0c60c80f961f0e71f3a9b524af6012062fe037a6');
23
+ });
24
+
25
+ await it('should derive key with sha512', async () => {
26
+ const key = pbkdf2Sync('password', 'salt', 1, 64, 'sha512');
27
+ expect(key).toBeDefined();
28
+ expect(key.length).toBe(64);
29
+ });
30
+
31
+ await it('should accept Buffer inputs', async () => {
32
+ const key = pbkdf2Sync(Buffer.from('password'), Buffer.from('salt'), 1000, 32, 'sha256');
33
+ expect(key.length).toBe(32);
34
+ });
35
+
36
+ await it('should handle keylen 0', async () => {
37
+ // Node.js native throws on keylen 0, our GJS impl returns empty buffer
38
+ // Both behaviors are acceptable — test that it doesn't crash
39
+ try {
40
+ const key = pbkdf2Sync('password', 'salt', 1, 0, 'sha256');
41
+ expect(key.length).toBe(0);
42
+ } catch (_e) {
43
+ // Node.js throws "Deriving bits failed" for keylen 0
44
+ expect(true).toBe(true);
45
+ }
46
+ });
47
+
48
+ await it('should throw on invalid iterations', async () => {
49
+ expect(() => {
50
+ pbkdf2Sync('password', 'salt', 0, 32, 'sha256');
51
+ }).toThrow();
52
+ });
53
+
54
+ await it('should throw on unsupported digest', async () => {
55
+ expect(() => {
56
+ pbkdf2Sync('password', 'salt', 1, 32, 'unsupported');
57
+ }).toThrow();
58
+ });
59
+ });
60
+
61
+ await describe('crypto.pbkdf2', async () => {
62
+ await it('should be a function', async () => {
63
+ expect(typeof pbkdf2).toBe('function');
64
+ });
65
+
66
+ await it('should derive key asynchronously', async () => {
67
+ await new Promise<void>((resolve) => {
68
+ pbkdf2('password', 'salt', 1000, 32, 'sha256', (err, key) => {
69
+ expect(err).toBeNull();
70
+ expect(key!.length).toBe(32);
71
+ resolve();
72
+ });
73
+ });
74
+ });
75
+ });
76
+ };
package/src/pbkdf2.ts ADDED
@@ -0,0 +1,106 @@
1
+ // Reference: Node.js lib/internal/crypto/pbkdf2.js, RFC 2898
2
+ // Reimplemented for GJS using pure-JS Hmac (over GLib.Checksum)
3
+
4
+ import { Buffer } from 'node:buffer';
5
+ import { Hmac } from './hmac.js';
6
+ import { normalizeAlgorithm, DIGEST_SIZES, SUPPORTED_ALGORITHMS, toBuffer } from './crypto-utils.js';
7
+
8
+ function hmacDigest(algo: string, key: Uint8Array, data: Uint8Array): Buffer {
9
+ const hmac = new Hmac(algo, key);
10
+ hmac.update(data);
11
+ return hmac.digest() as Buffer;
12
+ }
13
+
14
+ function validateParameters(iterations: number, keylen: number): void {
15
+ if (typeof iterations !== 'number' || iterations < 0 || !Number.isFinite(iterations)) {
16
+ throw new TypeError('iterations must be a positive number');
17
+ }
18
+ if (iterations === 0) {
19
+ throw new TypeError('iterations must be a positive number');
20
+ }
21
+ if (typeof keylen !== 'number' || keylen < 0 || !Number.isFinite(keylen) || keylen > 2147483647) {
22
+ throw new TypeError('keylen must be a positive number');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Synchronous PBKDF2 key derivation.
28
+ */
29
+ export function pbkdf2Sync(
30
+ password: string | Buffer | Uint8Array | DataView,
31
+ salt: string | Buffer | Uint8Array | DataView,
32
+ iterations: number,
33
+ keylen: number,
34
+ digest?: string
35
+ ): Buffer {
36
+ validateParameters(iterations, keylen);
37
+
38
+ const passwordBuf = toBuffer(password);
39
+ const saltBuf = toBuffer(salt);
40
+ const algo = normalizeAlgorithm(digest || 'sha1');
41
+ const hashLen = DIGEST_SIZES[algo];
42
+
43
+ if (!SUPPORTED_ALGORITHMS.has(algo) || hashLen === undefined) {
44
+ throw new TypeError(`Unknown message digest: ${digest || 'sha1'}`);
45
+ }
46
+
47
+ if (keylen === 0) {
48
+ return Buffer.alloc(0);
49
+ }
50
+
51
+ const numBlocks = Math.ceil(keylen / hashLen);
52
+ const dk = Buffer.allocUnsafe(numBlocks * hashLen);
53
+
54
+ // RFC 2898 Section 5.2
55
+ for (let blockIndex = 1; blockIndex <= numBlocks; blockIndex++) {
56
+ // U_1 = PRF(Password, Salt || INT_32_BE(i))
57
+ const block = Buffer.allocUnsafe(saltBuf.length + 4);
58
+ saltBuf.copy(block, 0);
59
+ block.writeUInt32BE(blockIndex, saltBuf.length);
60
+
61
+ let u = hmacDigest(algo, passwordBuf, block);
62
+ let t = Buffer.from(u);
63
+
64
+ // U_2 ... U_c
65
+ for (let iter = 1; iter < iterations; iter++) {
66
+ u = hmacDigest(algo, passwordBuf, u);
67
+ for (let k = 0; k < hashLen; k++) {
68
+ t[k] ^= u[k];
69
+ }
70
+ }
71
+
72
+ t.copy(dk, (blockIndex - 1) * hashLen);
73
+ }
74
+
75
+ // Return a proper Buffer slice
76
+ return Buffer.from(dk.buffer, dk.byteOffset, keylen);
77
+ }
78
+
79
+ /**
80
+ * Asynchronous PBKDF2 key derivation.
81
+ */
82
+ export function pbkdf2(
83
+ password: string | Buffer | Uint8Array | DataView,
84
+ salt: string | Buffer | Uint8Array | DataView,
85
+ iterations: number,
86
+ keylen: number,
87
+ digest: string,
88
+ callback: (err: Error | null, derivedKey?: Buffer) => void
89
+ ): void {
90
+ try {
91
+ validateParameters(iterations, keylen);
92
+ } catch (err) {
93
+ // Match Node.js behavior: validation errors are thrown synchronously
94
+ throw err;
95
+ }
96
+
97
+ // Run in next tick to be truly async
98
+ setTimeout(() => {
99
+ try {
100
+ const result = pbkdf2Sync(password, salt, iterations, keylen, digest);
101
+ callback(null, result);
102
+ } catch (err) {
103
+ callback(err instanceof Error ? err : new Error(String(err)));
104
+ }
105
+ }, 0);
106
+ }
@@ -0,0 +1,288 @@
1
+ // Public-key encryption — RSA PKCS#1 v1.5 for GJS
2
+ // Reference: refs/public-encrypt/publicEncrypt.js, refs/public-encrypt/privateDecrypt.js
3
+ // Reimplemented for GJS using native BigInt (ES2024)
4
+
5
+ import { Buffer } from 'node:buffer';
6
+ import { randomBytes } from './random.js';
7
+ import { parsePemKey, rsaKeySize } from './asn1.js';
8
+ import { modPow, bigIntToBytes, bytesToBigInt } from './bigint-math.js';
9
+
10
+ // ============================================================================
11
+ // Key extraction helpers
12
+ // ============================================================================
13
+
14
+ interface KeyInput {
15
+ key: string | Buffer;
16
+ passphrase?: string;
17
+ padding?: number;
18
+ }
19
+
20
+ function extractPem(key: string | Buffer | KeyInput): string {
21
+ if (typeof key === 'string') {
22
+ return key;
23
+ }
24
+ if (Buffer.isBuffer(key) || key instanceof Uint8Array) {
25
+ return Buffer.from(key).toString('utf8');
26
+ }
27
+ if (key && typeof key === 'object' && 'key' in key) {
28
+ const k = key.key;
29
+ if (typeof k === 'string') return k;
30
+ if (Buffer.isBuffer(k) || (k as unknown) instanceof Uint8Array) return Buffer.from(k as Uint8Array).toString('utf8');
31
+ }
32
+ throw new TypeError('Invalid key argument');
33
+ }
34
+
35
+ // ============================================================================
36
+ // PKCS#1 v1.5 padding
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Apply PKCS#1 v1.5 Type 2 padding (for encryption).
41
+ * Format: 0x00 0x02 [random non-zero bytes] 0x00 [data]
42
+ * The padding string (PS) must be at least 8 bytes.
43
+ */
44
+ function pkcs1v15Type2Pad(data: Uint8Array, keyLen: number): Uint8Array {
45
+ const maxDataLen = keyLen - 11; // 3 bytes overhead + 8 min padding
46
+ if (data.length > maxDataLen) {
47
+ throw new Error(`Data too long for key size. Max ${maxDataLen} bytes, got ${data.length}`);
48
+ }
49
+
50
+ const padLen = keyLen - data.length - 3;
51
+ const em = new Uint8Array(keyLen);
52
+ em[0] = 0x00;
53
+ em[1] = 0x02;
54
+
55
+ // Generate random non-zero padding bytes
56
+ const padding = randomBytes(padLen);
57
+ for (let i = 0; i < padLen; i++) {
58
+ // Ensure no zero bytes in the padding
59
+ while (padding[i] === 0) {
60
+ const replacement = randomBytes(1);
61
+ padding[i] = replacement[0];
62
+ }
63
+ em[2 + i] = padding[i];
64
+ }
65
+
66
+ em[2 + padLen] = 0x00;
67
+ em.set(data, 3 + padLen);
68
+
69
+ return em;
70
+ }
71
+
72
+ /**
73
+ * Apply PKCS#1 v1.5 Type 1 padding (for private-key encryption / signature).
74
+ * Format: 0x00 0x01 [0xFF bytes] 0x00 [data]
75
+ */
76
+ function pkcs1v15Type1Pad(data: Uint8Array, keyLen: number): Uint8Array {
77
+ const maxDataLen = keyLen - 11;
78
+ if (data.length > maxDataLen) {
79
+ throw new Error(`Data too long for key size. Max ${maxDataLen} bytes, got ${data.length}`);
80
+ }
81
+
82
+ const padLen = keyLen - data.length - 3;
83
+ const em = new Uint8Array(keyLen);
84
+ em[0] = 0x00;
85
+ em[1] = 0x01;
86
+ for (let i = 2; i < 2 + padLen; i++) {
87
+ em[i] = 0xff;
88
+ }
89
+ em[2 + padLen] = 0x00;
90
+ em.set(data, 3 + padLen);
91
+
92
+ return em;
93
+ }
94
+
95
+ /**
96
+ * Remove PKCS#1 v1.5 Type 2 padding (after decryption with private key).
97
+ * Expects: 0x00 0x02 [random non-zero bytes] 0x00 [data]
98
+ */
99
+ function pkcs1v15Type2Unpad(em: Uint8Array): Uint8Array {
100
+ if (em.length < 11) {
101
+ throw new Error('Decryption error: message too short');
102
+ }
103
+ if (em[0] !== 0x00 || em[1] !== 0x02) {
104
+ throw new Error('Decryption error: invalid padding');
105
+ }
106
+
107
+ // Find the 0x00 separator (must be at least at index 10 for 8 bytes of padding)
108
+ let sepIdx = 2;
109
+ while (sepIdx < em.length && em[sepIdx] !== 0x00) {
110
+ sepIdx++;
111
+ }
112
+ if (sepIdx >= em.length || sepIdx < 10) {
113
+ throw new Error('Decryption error: invalid padding');
114
+ }
115
+
116
+ return em.slice(sepIdx + 1);
117
+ }
118
+
119
+ /**
120
+ * Remove PKCS#1 v1.5 Type 1 padding (after decryption with public key).
121
+ * Expects: 0x00 0x01 [0xFF bytes] 0x00 [data]
122
+ */
123
+ function pkcs1v15Type1Unpad(em: Uint8Array): Uint8Array {
124
+ if (em.length < 11) {
125
+ throw new Error('Decryption error: message too short');
126
+ }
127
+ if (em[0] !== 0x00 || em[1] !== 0x01) {
128
+ throw new Error('Decryption error: invalid padding');
129
+ }
130
+
131
+ // Skip 0xFF bytes
132
+ let sepIdx = 2;
133
+ while (sepIdx < em.length && em[sepIdx] === 0xff) {
134
+ sepIdx++;
135
+ }
136
+ if (sepIdx >= em.length || em[sepIdx] !== 0x00 || sepIdx < 10) {
137
+ throw new Error('Decryption error: invalid padding');
138
+ }
139
+
140
+ return em.slice(sepIdx + 1);
141
+ }
142
+
143
+ // ============================================================================
144
+ // Public API
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Encrypt data using an RSA public key with PKCS#1 v1.5 Type 2 padding.
149
+ *
150
+ * @param key - PEM-encoded RSA public key (or private key, from which public components are extracted)
151
+ * @param buffer - Data to encrypt (must be <= keyLen - 11 bytes)
152
+ * @returns Encrypted data as a Buffer
153
+ */
154
+ export function publicEncrypt(key: string | Buffer | KeyInput, buffer: Buffer | Uint8Array): Buffer {
155
+ const pem = extractPem(key);
156
+ const parsed = parsePemKey(pem);
157
+
158
+ let n: bigint;
159
+ let e: bigint;
160
+ if (parsed.type === 'rsa-public') {
161
+ n = parsed.components.n;
162
+ e = parsed.components.e;
163
+ } else if (parsed.type === 'rsa-private') {
164
+ n = parsed.components.n;
165
+ e = parsed.components.e;
166
+ } else {
167
+ throw new Error('Key must be an RSA public or private key');
168
+ }
169
+
170
+ const keyLen = rsaKeySize(n);
171
+ const data = buffer instanceof Uint8Array ? buffer : Buffer.from(buffer);
172
+
173
+ // Apply PKCS#1 v1.5 Type 2 padding
174
+ const em = pkcs1v15Type2Pad(data, keyLen);
175
+
176
+ // RSA encrypt: c = m^e mod n
177
+ const m = bytesToBigInt(em);
178
+ const c = modPow(m, e, n);
179
+ return Buffer.from(bigIntToBytes(c, keyLen));
180
+ }
181
+
182
+ /**
183
+ * Decrypt data using an RSA private key (reverses publicEncrypt).
184
+ *
185
+ * @param key - PEM-encoded RSA private key
186
+ * @param buffer - Encrypted data
187
+ * @returns Decrypted data as a Buffer
188
+ */
189
+ export function privateDecrypt(key: string | Buffer | KeyInput, buffer: Buffer | Uint8Array): Buffer {
190
+ const pem = extractPem(key);
191
+ const parsed = parsePemKey(pem);
192
+
193
+ if (parsed.type !== 'rsa-private') {
194
+ throw new Error('Key must be an RSA private key');
195
+ }
196
+
197
+ const { n, d } = parsed.components;
198
+ const keyLen = rsaKeySize(n);
199
+ const data = buffer instanceof Uint8Array ? buffer : Buffer.from(buffer);
200
+
201
+ if (data.length !== keyLen) {
202
+ throw new Error(`Data length (${data.length}) does not match key length (${keyLen})`);
203
+ }
204
+
205
+ // RSA decrypt: m = c^d mod n
206
+ const c = bytesToBigInt(data);
207
+ if (c >= n) {
208
+ throw new Error('Decryption error: cipher value out of range');
209
+ }
210
+ const m = modPow(c, d, n);
211
+ const em = bigIntToBytes(m, keyLen);
212
+
213
+ // Remove PKCS#1 v1.5 Type 2 padding
214
+ return Buffer.from(pkcs1v15Type2Unpad(em));
215
+ }
216
+
217
+ /**
218
+ * Encrypt data using an RSA private key with PKCS#1 v1.5 Type 1 padding.
219
+ * This is the raw RSA private-key operation used for compatibility with
220
+ * signature-like use cases.
221
+ *
222
+ * @param key - PEM-encoded RSA private key
223
+ * @param buffer - Data to encrypt
224
+ * @returns Encrypted data as a Buffer
225
+ */
226
+ export function privateEncrypt(key: string | Buffer | KeyInput, buffer: Buffer | Uint8Array): Buffer {
227
+ const pem = extractPem(key);
228
+ const parsed = parsePemKey(pem);
229
+
230
+ if (parsed.type !== 'rsa-private') {
231
+ throw new Error('Key must be an RSA private key');
232
+ }
233
+
234
+ const { n, d } = parsed.components;
235
+ const keyLen = rsaKeySize(n);
236
+ const data = buffer instanceof Uint8Array ? buffer : Buffer.from(buffer);
237
+
238
+ // Apply PKCS#1 v1.5 Type 1 padding
239
+ const em = pkcs1v15Type1Pad(data, keyLen);
240
+
241
+ // RSA private key operation: c = m^d mod n
242
+ const m = bytesToBigInt(em);
243
+ const c = modPow(m, d, n);
244
+ return Buffer.from(bigIntToBytes(c, keyLen));
245
+ }
246
+
247
+ /**
248
+ * Decrypt data using an RSA public key (reverses privateEncrypt).
249
+ * Removes PKCS#1 v1.5 Type 1 padding.
250
+ *
251
+ * @param key - PEM-encoded RSA public key (or private key for public components)
252
+ * @param buffer - Encrypted data
253
+ * @returns Decrypted data as a Buffer
254
+ */
255
+ export function publicDecrypt(key: string | Buffer | KeyInput, buffer: Buffer | Uint8Array): Buffer {
256
+ const pem = extractPem(key);
257
+ const parsed = parsePemKey(pem);
258
+
259
+ let n: bigint;
260
+ let e: bigint;
261
+ if (parsed.type === 'rsa-public') {
262
+ n = parsed.components.n;
263
+ e = parsed.components.e;
264
+ } else if (parsed.type === 'rsa-private') {
265
+ n = parsed.components.n;
266
+ e = parsed.components.e;
267
+ } else {
268
+ throw new Error('Key must be an RSA public or private key');
269
+ }
270
+
271
+ const keyLen = rsaKeySize(n);
272
+ const data = buffer instanceof Uint8Array ? buffer : Buffer.from(buffer);
273
+
274
+ if (data.length !== keyLen) {
275
+ throw new Error(`Data length (${data.length}) does not match key length (${keyLen})`);
276
+ }
277
+
278
+ // RSA public key operation: m = c^e mod n
279
+ const c = bytesToBigInt(data);
280
+ if (c >= n) {
281
+ throw new Error('Decryption error: cipher value out of range');
282
+ }
283
+ const m = modPow(c, e, n);
284
+ const em = bigIntToBytes(m, keyLen);
285
+
286
+ // Remove PKCS#1 v1.5 Type 1 padding
287
+ return Buffer.from(pkcs1v15Type1Unpad(em));
288
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { randomBytes, randomUUID, randomInt, randomFillSync, timingSafeEqual } from 'node:crypto';
3
+ import { Buffer } from 'node:buffer';
4
+
5
+ export default async () => {
6
+ await describe('crypto.randomBytes', async () => {
7
+ await it('should return a Buffer of requested size', async () => {
8
+ const buf = randomBytes(32);
9
+ expect(buf.length).toBe(32);
10
+ });
11
+
12
+ await it('should return empty buffer for size 0', async () => {
13
+ const buf = randomBytes(0);
14
+ expect(buf.length).toBe(0);
15
+ });
16
+
17
+ await it('should return different values on each call', async () => {
18
+ const a = randomBytes(16);
19
+ const b = randomBytes(16);
20
+ // Extremely unlikely to be equal
21
+ let equal = true;
22
+ for (let i = 0; i < 16; i++) {
23
+ if (a[i] !== b[i]) { equal = false; break; }
24
+ }
25
+ expect(equal).toBe(false);
26
+ });
27
+
28
+ await it('should work with callback', async () => {
29
+ const result = await new Promise<Buffer>((resolve, reject) => {
30
+ randomBytes(16, (err, buf) => {
31
+ if (err) reject(err);
32
+ else resolve(buf);
33
+ });
34
+ });
35
+ expect(result.length).toBe(16);
36
+ });
37
+
38
+ await it('should handle large sizes (> 65536)', async () => {
39
+ const buf = randomBytes(100000);
40
+ expect(buf.length).toBe(100000);
41
+ });
42
+ });
43
+
44
+ await describe('crypto.randomUUID', async () => {
45
+ await it('should return a valid UUID v4 string', async () => {
46
+ const uuid = randomUUID();
47
+ expect(uuid.length).toBe(36);
48
+ expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
49
+ });
50
+
51
+ await it('should return different UUIDs', async () => {
52
+ const a = randomUUID();
53
+ const b = randomUUID();
54
+ expect(a).not.toBe(b);
55
+ });
56
+ });
57
+
58
+ await describe('crypto.randomInt', async () => {
59
+ await it('should return integer in range [0, max)', async () => {
60
+ for (let i = 0; i < 20; i++) {
61
+ const val = randomInt(10) as number;
62
+ expect(val).toBeGreaterThan(-1);
63
+ expect(val).toBeLessThan(10);
64
+ }
65
+ });
66
+
67
+ await it('should return integer in range [min, max)', async () => {
68
+ for (let i = 0; i < 20; i++) {
69
+ const val = randomInt(5, 10) as number;
70
+ expect(val).toBeGreaterThan(4);
71
+ expect(val).toBeLessThan(10);
72
+ }
73
+ });
74
+
75
+ await it('should throw when min >= max', async () => {
76
+ let threw = false;
77
+ try {
78
+ randomInt(10, 5);
79
+ } catch {
80
+ threw = true;
81
+ }
82
+ expect(threw).toBe(true);
83
+ });
84
+ });
85
+
86
+ await describe('crypto.randomFillSync', async () => {
87
+ await it('should fill a buffer with random data', async () => {
88
+ const buf = Buffer.alloc(16);
89
+ randomFillSync(buf);
90
+ let allZero = true;
91
+ for (let i = 0; i < 16; i++) {
92
+ if (buf[i] !== 0) { allZero = false; break; }
93
+ }
94
+ expect(allZero).toBe(false);
95
+ });
96
+
97
+ await it('should fill with offset and size', async () => {
98
+ const buf = Buffer.alloc(16, 0);
99
+ randomFillSync(buf, 4, 8);
100
+ // First 4 bytes should be zero
101
+ expect(buf[0]).toBe(0);
102
+ expect(buf[1]).toBe(0);
103
+ expect(buf[2]).toBe(0);
104
+ expect(buf[3]).toBe(0);
105
+ });
106
+ });
107
+
108
+ await describe('crypto.timingSafeEqual', async () => {
109
+ await it('should return true for equal buffers', async () => {
110
+ const a = Buffer.from('hello');
111
+ const b = Buffer.from('hello');
112
+ expect(timingSafeEqual(a, b)).toBe(true);
113
+ });
114
+
115
+ await it('should return false for different buffers', async () => {
116
+ const a = Buffer.from('hello');
117
+ const b = Buffer.from('world');
118
+ expect(timingSafeEqual(a, b)).toBe(false);
119
+ });
120
+
121
+ await it('should throw for different lengths', async () => {
122
+ const a = Buffer.from('hello');
123
+ const b = Buffer.from('hi');
124
+ let threw = false;
125
+ try {
126
+ timingSafeEqual(a, b);
127
+ } catch {
128
+ threw = true;
129
+ }
130
+ expect(threw).toBe(true);
131
+ });
132
+ });
133
+ };