@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/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
|
+
}
|
package/src/sign.spec.ts
ADDED
|
@@ -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 });
|