@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/random.ts ADDED
@@ -0,0 +1,183 @@
1
+ // Reference: Node.js lib/internal/crypto/random.js
2
+ // Reimplemented for GJS using WebCrypto API / GLib.Random
3
+
4
+ import { Buffer } from 'node:buffer';
5
+
6
+ const hasWebCrypto = typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.getRandomValues === 'function';
7
+
8
+ /**
9
+ * Fill a Uint8Array with random bytes, using WebCrypto or GLib.Random fallback.
10
+ */
11
+ function fillRandom(view: Uint8Array): void {
12
+ if (hasWebCrypto) {
13
+ // WebCrypto has a 65536-byte limit per call
14
+ for (let offset = 0; offset < view.length; offset += 65536) {
15
+ const length = Math.min(view.length - offset, 65536);
16
+ const slice = new Uint8Array(view.buffer as ArrayBuffer, view.byteOffset + offset, length);
17
+ globalThis.crypto.getRandomValues(slice);
18
+ }
19
+ } else {
20
+ // GLib.Random fallback for GJS environments without WebCrypto
21
+ try {
22
+ const GLib = imports.gi.GLib;
23
+ for (let i = 0; i < view.length; i++) {
24
+ view[i] = GLib.random_int_range(0, 256);
25
+ }
26
+ } catch {
27
+ // Last resort: Math.random (not cryptographically secure)
28
+ for (let i = 0; i < view.length; i++) {
29
+ view[i] = Math.floor(Math.random() * 256);
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Generate cryptographically strong pseudo-random data.
37
+ */
38
+ export function randomBytes(size: number): Buffer;
39
+ export function randomBytes(size: number, callback: (err: Error | null, buf: Buffer) => void): void;
40
+ export function randomBytes(size: number, callback?: (err: Error | null, buf: Buffer) => void): Buffer | void {
41
+ if (typeof size !== 'number' || size < 0 || !Number.isInteger(size)) {
42
+ throw new TypeError(`The "size" argument must be a non-negative integer. Received ${size}`);
43
+ }
44
+
45
+ try {
46
+ const buf = Buffer.alloc(size);
47
+ if (size > 0) {
48
+ fillRandom(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
49
+ }
50
+ if (callback) {
51
+ callback(null, buf);
52
+ } else {
53
+ return buf;
54
+ }
55
+ } catch (err) {
56
+ if (callback) {
57
+ callback(err as Error, Buffer.alloc(0));
58
+ } else {
59
+ throw err;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Fill a buffer with cryptographically strong pseudo-random data (synchronous).
66
+ */
67
+ export function randomFillSync(buffer: Buffer | Uint8Array, offset = 0, size?: number): Buffer | Uint8Array {
68
+ const length = size ?? (buffer.length - offset);
69
+
70
+ if (offset < 0 || offset > buffer.length) {
71
+ throw new RangeError(`The value of "offset" is out of range. Received ${offset}`);
72
+ }
73
+ if (length < 0 || offset + length > buffer.length) {
74
+ throw new RangeError(`The value of "size" is out of range. Received ${length}`);
75
+ }
76
+
77
+ if (length > 0) {
78
+ const byteOffset = buffer instanceof Buffer ? buffer.byteOffset : buffer.byteOffset;
79
+ const view = new Uint8Array(buffer.buffer as ArrayBuffer, byteOffset + offset, length);
80
+ fillRandom(view);
81
+ }
82
+
83
+ return buffer;
84
+ }
85
+
86
+ /**
87
+ * Fill a buffer with cryptographically strong pseudo-random data (async).
88
+ */
89
+ export function randomFill(
90
+ buffer: Buffer | Uint8Array,
91
+ offset: number | ((err: Error | null, buf: Buffer | Uint8Array) => void),
92
+ size?: number | ((err: Error | null, buf: Buffer | Uint8Array) => void),
93
+ callback?: (err: Error | null, buf: Buffer | Uint8Array) => void,
94
+ ): void {
95
+ let _offset: number;
96
+ let _size: number;
97
+ let _callback: (err: Error | null, buf: Buffer | Uint8Array) => void;
98
+
99
+ if (typeof offset === 'function') {
100
+ _callback = offset;
101
+ _offset = 0;
102
+ _size = buffer.length;
103
+ } else if (typeof size === 'function') {
104
+ _callback = size;
105
+ _offset = offset;
106
+ _size = buffer.length - offset;
107
+ } else {
108
+ _callback = callback!;
109
+ _offset = offset;
110
+ _size = size!;
111
+ }
112
+
113
+ try {
114
+ randomFillSync(buffer, _offset, _size);
115
+ _callback(null, buffer);
116
+ } catch (err) {
117
+ _callback(err as Error, buffer);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Generate a random UUID v4 string.
123
+ */
124
+ export function randomUUID(): string {
125
+ if (hasWebCrypto && typeof globalThis.crypto.randomUUID === 'function') {
126
+ return globalThis.crypto.randomUUID();
127
+ }
128
+
129
+ // Manual UUID v4 generation fallback
130
+ const bytes = new Uint8Array(16);
131
+ fillRandom(bytes);
132
+ // Set version (4) and variant (10xx)
133
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
134
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
135
+
136
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
137
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
138
+ }
139
+
140
+ /**
141
+ * Generate a random integer between min (inclusive) and max (exclusive).
142
+ */
143
+ export function randomInt(min: number, max?: number | ((err: Error | null, value: number) => void), callback?: (err: Error | null, value: number) => void): number | void {
144
+ let _min: number;
145
+ let _max: number;
146
+ let _callback: ((err: Error | null, value: number) => void) | undefined;
147
+
148
+ if (typeof max === 'function') {
149
+ _callback = max;
150
+ _max = min;
151
+ _min = 0;
152
+ } else if (typeof max === 'number') {
153
+ _min = min;
154
+ _max = max;
155
+ _callback = callback;
156
+ } else {
157
+ _max = min;
158
+ _min = 0;
159
+ _callback = callback;
160
+ }
161
+
162
+ if (!Number.isInteger(_min)) {
163
+ throw new TypeError(`The "min" argument must be a safe integer. Received ${_min}`);
164
+ }
165
+ if (!Number.isInteger(_max)) {
166
+ throw new TypeError(`The "max" argument must be a safe integer. Received ${_max}`);
167
+ }
168
+ if (_min >= _max) {
169
+ throw new RangeError(`The value of "min" must be less than "max". Received min: ${_min}, max: ${_max}`);
170
+ }
171
+
172
+ const range = _max - _min;
173
+ const bytes = new Uint32Array(1);
174
+ const view = new Uint8Array(bytes.buffer);
175
+ fillRandom(view);
176
+ const value = _min + (bytes[0] % range);
177
+
178
+ if (_callback) {
179
+ _callback(null, value);
180
+ } else {
181
+ return value;
182
+ }
183
+ }
@@ -0,0 +1,167 @@
1
+ // RSA-OAEP (Optimal Asymmetric Encryption Padding) per RFC 8017 Section 7.1
2
+ // Reference: refs/node/lib/internal/crypto/cipher.js
3
+ // Copyright (c) Node.js contributors. MIT license.
4
+ // Reimplemented for GJS using BigInt RSA math
5
+
6
+ import { Hash } from './hash.js';
7
+ import { randomBytes } from './random.js';
8
+ import { mgf1 } from './mgf1.js';
9
+ import { parsePemKey, rsaKeySize } from './asn1.js';
10
+ import type { RsaPrivateComponents, RsaPublicComponents } from './asn1.js';
11
+ import { hashSize } from './crypto-utils.js';
12
+ import { modPow, bigIntToBytes, bytesToBigInt } from './bigint-math.js';
13
+
14
+ function hashDigest(algo: string, data: Uint8Array): Uint8Array {
15
+ const h = new Hash(algo);
16
+ h.update(data);
17
+ return new Uint8Array(h.digest() as any);
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // RSAES-OAEP-ENCRYPT (RFC 8017 Section 7.1.1)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * RSA-OAEP encrypt using PEM public key.
26
+ */
27
+ export function rsaOaepEncrypt(
28
+ hashAlgo: string,
29
+ pubKeyPem: string,
30
+ plaintext: Uint8Array,
31
+ label?: Uint8Array,
32
+ ): Uint8Array {
33
+ const parsed = parsePemKey(pubKeyPem);
34
+ let n: bigint, e: bigint;
35
+ if (parsed.type === 'rsa-public') {
36
+ ({ n, e } = parsed.components as RsaPublicComponents);
37
+ } else if (parsed.type === 'rsa-private') {
38
+ ({ n, e } = parsed.components as RsaPrivateComponents);
39
+ } else {
40
+ throw new Error('RSA-OAEP: expected RSA key');
41
+ }
42
+
43
+ const keyBytes = Math.ceil(rsaKeySize(n) / 8);
44
+ const hLen = hashSize(hashAlgo);
45
+
46
+ if (plaintext.length > keyBytes - 2 * hLen - 2) {
47
+ throw new Error('RSA-OAEP: message too long');
48
+ }
49
+
50
+ // lHash = Hash(L) where L = label (empty by default)
51
+ const lHash = hashDigest(hashAlgo, label || new Uint8Array(0));
52
+
53
+ // DB = lHash || PS || 0x01 || M
54
+ const dbLen = keyBytes - hLen - 1;
55
+ const DB = new Uint8Array(dbLen);
56
+ DB.set(lHash, 0);
57
+ DB[dbLen - plaintext.length - 1] = 0x01;
58
+ DB.set(plaintext, dbLen - plaintext.length);
59
+
60
+ // seed = random bytes of length hLen
61
+ const seed = new Uint8Array(randomBytes(hLen));
62
+
63
+ // dbMask = MGF(seed, dbLen)
64
+ const dbMask = mgf1(hashAlgo, seed, dbLen);
65
+
66
+ // maskedDB = DB ⊕ dbMask
67
+ const maskedDB = new Uint8Array(dbLen);
68
+ for (let i = 0; i < dbLen; i++) maskedDB[i] = DB[i] ^ dbMask[i];
69
+
70
+ // seedMask = MGF(maskedDB, hLen)
71
+ const seedMask = mgf1(hashAlgo, maskedDB, hLen);
72
+
73
+ // maskedSeed = seed ⊕ seedMask
74
+ const maskedSeed = new Uint8Array(hLen);
75
+ for (let i = 0; i < hLen; i++) maskedSeed[i] = seed[i] ^ seedMask[i];
76
+
77
+ // EM = 0x00 || maskedSeed || maskedDB
78
+ const EM = new Uint8Array(keyBytes);
79
+ EM[0] = 0x00;
80
+ EM.set(maskedSeed, 1);
81
+ EM.set(maskedDB, 1 + hLen);
82
+
83
+ // RSAEP: c = m^e mod n
84
+ const m = bytesToBigInt(EM);
85
+ const c = modPow(m, e, n);
86
+ return bigIntToBytes(c, keyBytes);
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // RSAES-OAEP-DECRYPT (RFC 8017 Section 7.1.2)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * RSA-OAEP decrypt using PEM private key.
95
+ */
96
+ export function rsaOaepDecrypt(
97
+ hashAlgo: string,
98
+ privKeyPem: string,
99
+ ciphertext: Uint8Array,
100
+ label?: Uint8Array,
101
+ ): Uint8Array {
102
+ const parsed = parsePemKey(privKeyPem);
103
+ if (parsed.type !== 'rsa-private') throw new Error('RSA-OAEP: expected RSA private key');
104
+ const { n, d } = parsed.components as RsaPrivateComponents;
105
+
106
+ const keyBytes = Math.ceil(rsaKeySize(n) / 8);
107
+ const hLen = hashSize(hashAlgo);
108
+
109
+ if (ciphertext.length !== keyBytes || keyBytes < 2 * hLen + 2) {
110
+ throw new Error('RSA-OAEP: decryption error');
111
+ }
112
+
113
+ // RSADP: m = c^d mod n
114
+ const c = bytesToBigInt(ciphertext);
115
+ const m = modPow(c, d, n);
116
+ const EM = bigIntToBytes(m, keyBytes);
117
+
118
+ // Y = EM[0], maskedSeed = EM[1..hLen], maskedDB = EM[1+hLen..]
119
+ const Y = EM[0];
120
+ const maskedSeed = EM.slice(1, 1 + hLen);
121
+ const maskedDB = EM.slice(1 + hLen);
122
+ const dbLen = keyBytes - hLen - 1;
123
+
124
+ // seedMask = MGF(maskedDB, hLen)
125
+ const seedMask = mgf1(hashAlgo, maskedDB, hLen);
126
+
127
+ // seed = maskedSeed ⊕ seedMask
128
+ const seed = new Uint8Array(hLen);
129
+ for (let i = 0; i < hLen; i++) seed[i] = maskedSeed[i] ^ seedMask[i];
130
+
131
+ // dbMask = MGF(seed, dbLen)
132
+ const dbMask = mgf1(hashAlgo, seed, dbLen);
133
+
134
+ // DB = maskedDB ⊕ dbMask
135
+ const DB = new Uint8Array(dbLen);
136
+ for (let i = 0; i < dbLen; i++) DB[i] = maskedDB[i] ^ dbMask[i];
137
+
138
+ // lHash' = DB[0..hLen-1]
139
+ const lHash = hashDigest(hashAlgo, label || new Uint8Array(0));
140
+ const lHashPrime = DB.slice(0, hLen);
141
+
142
+ // Verify Y == 0 and lHash == lHash'
143
+ let valid = Y === 0 ? 1 : 0;
144
+ for (let i = 0; i < hLen; i++) {
145
+ valid &= (lHash[i] === lHashPrime[i]) ? 1 : 0;
146
+ }
147
+
148
+ // Find 0x01 separator
149
+ let sepIdx = -1;
150
+ for (let i = hLen; i < dbLen; i++) {
151
+ if (DB[i] === 0x01) {
152
+ sepIdx = i;
153
+ break;
154
+ } else if (DB[i] !== 0x00) {
155
+ valid = 0;
156
+ break;
157
+ }
158
+ }
159
+
160
+ if (sepIdx === -1) valid = 0;
161
+
162
+ if (!valid) {
163
+ throw new Error('RSA-OAEP: decryption error');
164
+ }
165
+
166
+ return DB.slice(sepIdx + 1);
167
+ }
package/src/rsa-pss.ts ADDED
@@ -0,0 +1,190 @@
1
+ // RSA-PSS (Probabilistic Signature Scheme) per RFC 8017 Section 8.1
2
+ // Reference: refs/node/lib/internal/crypto/sig.js
3
+ // Copyright (c) Node.js contributors. MIT license.
4
+ // Reimplemented for GJS using BigInt RSA math
5
+
6
+ import { Hash } from './hash.js';
7
+ import { randomBytes } from './random.js';
8
+ import { mgf1 } from './mgf1.js';
9
+ import { parsePemKey, rsaKeySize } from './asn1.js';
10
+ import type { RsaPrivateComponents, RsaPublicComponents } from './asn1.js';
11
+ import { hashSize } from './crypto-utils.js';
12
+ import { modPow, bigIntToBytes, bytesToBigInt } from './bigint-math.js';
13
+
14
+ function hashDigest(algo: string, data: Uint8Array): Uint8Array {
15
+ const h = new Hash(algo);
16
+ h.update(data);
17
+ return new Uint8Array(h.digest() as any);
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // EMSA-PSS-ENCODE (RFC 8017 Section 9.1.1)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function emsaPssEncode(
25
+ mHash: Uint8Array,
26
+ emBits: number,
27
+ hashAlgo: string,
28
+ saltLength: number,
29
+ ): Uint8Array {
30
+ const hLen = hashSize(hashAlgo);
31
+ const emLen = Math.ceil(emBits / 8);
32
+
33
+ if (emLen < hLen + saltLength + 2) {
34
+ throw new Error('RSA-PSS: encoding error — key too short');
35
+ }
36
+
37
+ const salt = saltLength > 0 ? new Uint8Array(randomBytes(saltLength)) : new Uint8Array(0);
38
+
39
+ // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt
40
+ const mPrime = new Uint8Array(8 + hLen + saltLength);
41
+ mPrime.set(mHash, 8);
42
+ mPrime.set(salt, 8 + hLen);
43
+
44
+ const H = hashDigest(hashAlgo, mPrime);
45
+
46
+ // DB = PS || 0x01 || salt (PS = zero bytes of length emLen - hLen - saltLength - 2)
47
+ const dbLen = emLen - hLen - 1;
48
+ const DB = new Uint8Array(dbLen);
49
+ DB[dbLen - saltLength - 1] = 0x01;
50
+ DB.set(salt, dbLen - saltLength);
51
+
52
+ // dbMask = MGF(H, emLen - hLen - 1)
53
+ const dbMask = mgf1(hashAlgo, H, dbLen);
54
+
55
+ // maskedDB = DB ⊕ dbMask
56
+ const maskedDB = new Uint8Array(dbLen);
57
+ for (let i = 0; i < dbLen; i++) maskedDB[i] = DB[i] ^ dbMask[i];
58
+
59
+ // Set leftmost (8*emLen - emBits) bits of maskedDB to zero
60
+ const zeroBits = 8 * emLen - emBits;
61
+ if (zeroBits > 0) maskedDB[0] &= (0xff >>> zeroBits);
62
+
63
+ // EM = maskedDB || H || 0xbc
64
+ const EM = new Uint8Array(emLen);
65
+ EM.set(maskedDB, 0);
66
+ EM.set(H, dbLen);
67
+ EM[emLen - 1] = 0xbc;
68
+
69
+ return EM;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // EMSA-PSS-VERIFY (RFC 8017 Section 9.1.2)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function emsaPssVerify(
77
+ mHash: Uint8Array,
78
+ EM: Uint8Array,
79
+ emBits: number,
80
+ hashAlgo: string,
81
+ saltLength: number,
82
+ ): boolean {
83
+ const hLen = hashSize(hashAlgo);
84
+ const emLen = Math.ceil(emBits / 8);
85
+
86
+ if (emLen < hLen + saltLength + 2) return false;
87
+ if (EM[emLen - 1] !== 0xbc) return false;
88
+
89
+ const dbLen = emLen - hLen - 1;
90
+ const maskedDB = EM.slice(0, dbLen);
91
+ const H = EM.slice(dbLen, dbLen + hLen);
92
+
93
+ // Check leftmost bits
94
+ const zeroBits = 8 * emLen - emBits;
95
+ if (zeroBits > 0 && (maskedDB[0] & (0xff << (8 - zeroBits))) !== 0) return false;
96
+
97
+ // dbMask = MGF(H, dbLen)
98
+ const dbMask = mgf1(hashAlgo, H, dbLen);
99
+
100
+ // DB = maskedDB ⊕ dbMask
101
+ const DB = new Uint8Array(dbLen);
102
+ for (let i = 0; i < dbLen; i++) DB[i] = maskedDB[i] ^ dbMask[i];
103
+
104
+ // Zero leftmost bits
105
+ if (zeroBits > 0) DB[0] &= (0xff >>> zeroBits);
106
+
107
+ // Check PS: DB[0..dbLen-saltLength-2] should all be 0x00
108
+ for (let i = 0; i < dbLen - saltLength - 1; i++) {
109
+ if (DB[i] !== 0x00) return false;
110
+ }
111
+ if (DB[dbLen - saltLength - 1] !== 0x01) return false;
112
+
113
+ const salt = DB.slice(dbLen - saltLength);
114
+
115
+ // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt
116
+ const mPrime = new Uint8Array(8 + hLen + saltLength);
117
+ mPrime.set(mHash, 8);
118
+ mPrime.set(salt, 8 + hLen);
119
+
120
+ const HPrime = hashDigest(hashAlgo, mPrime);
121
+
122
+ // Compare H and H'
123
+ if (H.length !== HPrime.length) return false;
124
+ let diff = 0;
125
+ for (let i = 0; i < H.length; i++) diff |= H[i] ^ HPrime[i];
126
+ return diff === 0;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // RSA-PSS Sign / Verify (operating on raw BigInt components)
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * RSA-PSS sign using PEM private key.
135
+ */
136
+ export function rsaPssSign(
137
+ hashAlgo: string,
138
+ privKeyPem: string,
139
+ data: Uint8Array,
140
+ saltLength: number,
141
+ ): Uint8Array {
142
+ const parsed = parsePemKey(privKeyPem);
143
+ if (parsed.type !== 'rsa-private') throw new Error('RSA-PSS: expected RSA private key');
144
+ const { n, d } = parsed.components as RsaPrivateComponents;
145
+
146
+ const keyBits = rsaKeySize(n);
147
+ const keyBytes = Math.ceil(keyBits / 8);
148
+
149
+ const mHash = hashDigest(hashAlgo, data);
150
+ const EM = emsaPssEncode(mHash, keyBits - 1, hashAlgo, saltLength);
151
+
152
+ // RSASP1: s = EM^d mod n
153
+ const m = bytesToBigInt(EM);
154
+ const s = modPow(m, d, n);
155
+ return bigIntToBytes(s, keyBytes);
156
+ }
157
+
158
+ /**
159
+ * RSA-PSS verify using PEM public key.
160
+ */
161
+ export function rsaPssVerify(
162
+ hashAlgo: string,
163
+ pubKeyPem: string,
164
+ signature: Uint8Array,
165
+ data: Uint8Array,
166
+ saltLength: number,
167
+ ): boolean {
168
+ const parsed = parsePemKey(pubKeyPem);
169
+ let n: bigint, e: bigint;
170
+ if (parsed.type === 'rsa-public') {
171
+ ({ n, e } = parsed.components as RsaPublicComponents);
172
+ } else if (parsed.type === 'rsa-private') {
173
+ ({ n, e } = parsed.components as RsaPrivateComponents);
174
+ } else {
175
+ throw new Error('RSA-PSS: expected RSA key');
176
+ }
177
+
178
+ const keyBits = rsaKeySize(n);
179
+ const keyBytes = Math.ceil(keyBits / 8);
180
+
181
+ if (signature.length !== keyBytes) return false;
182
+
183
+ // RSAVP1: m = s^e mod n
184
+ const s = bytesToBigInt(signature);
185
+ const m = modPow(s, e, n);
186
+ const EM = bigIntToBytes(m, Math.ceil((keyBits - 1) / 8));
187
+
188
+ const mHash = hashDigest(hashAlgo, data);
189
+ return emsaPssVerify(mHash, EM, keyBits - 1, hashAlgo, saltLength);
190
+ }
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { scrypt, scryptSync } from 'node:crypto';
3
+ import { Buffer } from 'node:buffer';
4
+
5
+ export default async () => {
6
+ await describe('crypto.scryptSync', async () => {
7
+ await it('should be a function', async () => {
8
+ expect(typeof scryptSync).toBe('function');
9
+ });
10
+
11
+ await it('should derive a 64-byte key with default options', async () => {
12
+ const key = scryptSync('password', 'salt', 64);
13
+ expect(key).toBeDefined();
14
+ expect(key.length).toBe(64);
15
+ expect(key.toString('hex').substring(0, 64)).toBe(
16
+ '745731af4484f323968969eda289aeee005b5903ac561e64a5aca121797bf773'
17
+ );
18
+ });
19
+
20
+ await it('should derive a 32-byte key', async () => {
21
+ const key = scryptSync('password', 'salt', 32);
22
+ expect(key).toBeDefined();
23
+ expect(key.length).toBe(32);
24
+ });
25
+
26
+ await it('should accept Buffer inputs', async () => {
27
+ const key = scryptSync(Buffer.from('password'), Buffer.from('salt'), 64);
28
+ expect(key.length).toBe(64);
29
+ // Same result as string input
30
+ expect(key.toString('hex').substring(0, 64)).toBe(
31
+ '745731af4484f323968969eda289aeee005b5903ac561e64a5aca121797bf773'
32
+ );
33
+ });
34
+
35
+ await it('should match RFC 7914 test vector (N=1024, r=8, p=16)', async () => {
36
+ const key = scryptSync('password', 'NaCl', 64, { N: 1024, r: 8, p: 16 });
37
+ expect(key.toString('hex')).toBe(
38
+ 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640'
39
+ );
40
+ });
41
+
42
+ await it('should produce different keys for different passwords', async () => {
43
+ const key1 = scryptSync('password1', 'salt', 32);
44
+ const key2 = scryptSync('password2', 'salt', 32);
45
+ expect(key1.toString('hex') !== key2.toString('hex')).toBe(true);
46
+ });
47
+
48
+ await it('should produce different keys for different salts', async () => {
49
+ const key1 = scryptSync('password', 'salt1', 32);
50
+ const key2 = scryptSync('password', 'salt2', 32);
51
+ expect(key1.toString('hex') !== key2.toString('hex')).toBe(true);
52
+ });
53
+
54
+ await it('should accept custom cost parameter N', async () => {
55
+ const key = scryptSync('test', 'salt', 32, { N: 256 });
56
+ expect(key.length).toBe(32);
57
+ });
58
+ });
59
+
60
+ await describe('crypto.scrypt', async () => {
61
+ await it('should be a function', async () => {
62
+ expect(typeof scrypt).toBe('function');
63
+ });
64
+
65
+ await it('should derive key asynchronously', async () => {
66
+ const key = await new Promise<Buffer>((resolve, reject) => {
67
+ scrypt('password', 'salt', 64, (err, key) => {
68
+ if (err) reject(err);
69
+ else resolve(key);
70
+ });
71
+ });
72
+ expect(key.length).toBe(64);
73
+ expect(key.toString('hex').substring(0, 64)).toBe(
74
+ '745731af4484f323968969eda289aeee005b5903ac561e64a5aca121797bf773'
75
+ );
76
+ });
77
+
78
+ await it('should accept options parameter', async () => {
79
+ const key = await new Promise<Buffer>((resolve, reject) => {
80
+ scrypt('password', 'NaCl', 64, { N: 1024, r: 8, p: 16 }, (err, key) => {
81
+ if (err) reject(err);
82
+ else resolve(key);
83
+ });
84
+ });
85
+ expect(key.toString('hex')).toBe(
86
+ 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640'
87
+ );
88
+ });
89
+ });
90
+ };