@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/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
|
+
};
|