@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
@@ -0,0 +1,141 @@
1
+ // Tests for AES-GCM cipher mode
2
+ // Reference: NIST SP 800-38D test vectors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import { createCipheriv, createDecipheriv } from 'node:crypto';
6
+ import { Buffer } from 'node:buffer';
7
+
8
+ export default async () => {
9
+
10
+ await describe('AES-GCM', async () => {
11
+ await it('should support aes-256-gcm algorithm', async () => {
12
+ const key = Buffer.alloc(32, 'a');
13
+ const iv = Buffer.alloc(12, 'b');
14
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
15
+ expect(cipher).toBeDefined();
16
+ });
17
+
18
+ await it('should support aes-128-gcm algorithm', async () => {
19
+ const key = Buffer.alloc(16, 'a');
20
+ const iv = Buffer.alloc(12, 'b');
21
+ const cipher = createCipheriv('aes-128-gcm', key, iv);
22
+ expect(cipher).toBeDefined();
23
+ });
24
+
25
+ await it('should encrypt and decrypt round-trip', async () => {
26
+ const key = Buffer.from('0123456789abcdef0123456789abcdef', 'utf8');
27
+ const iv = Buffer.alloc(12, 0);
28
+ const plaintext = 'Hello, AES-GCM!';
29
+
30
+ // Encrypt
31
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
32
+ let encrypted = cipher.update(plaintext, 'utf8');
33
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
34
+ const tag = cipher.getAuthTag();
35
+ expect(tag).toBeDefined();
36
+ expect(tag.length).toBe(16);
37
+
38
+ // Decrypt
39
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
40
+ decipher.setAuthTag(tag);
41
+ let decrypted = decipher.update(encrypted);
42
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
43
+
44
+ expect(decrypted.toString('utf8')).toBe(plaintext);
45
+ });
46
+
47
+ await it('should fail with wrong auth tag', async () => {
48
+ const key = Buffer.alloc(32, 'k');
49
+ const iv = Buffer.alloc(12, 'i');
50
+ const plaintext = 'secret data';
51
+
52
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
53
+ let encrypted = cipher.update(plaintext, 'utf8');
54
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
55
+ const tag = cipher.getAuthTag();
56
+
57
+ // Tamper with the tag
58
+ const badTag = Buffer.from(tag);
59
+ badTag[0] ^= 0xff;
60
+
61
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
62
+ decipher.setAuthTag(badTag);
63
+ decipher.update(encrypted);
64
+ expect(() => decipher.final()).toThrow();
65
+ });
66
+
67
+ await it('should support AAD (additional authenticated data)', async () => {
68
+ const key = Buffer.alloc(32, 'k');
69
+ const iv = Buffer.alloc(12, 'i');
70
+ const plaintext = 'encrypted part';
71
+ const aad = Buffer.from('authenticated but not encrypted');
72
+
73
+ // Encrypt with AAD
74
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
75
+ cipher.setAAD(aad);
76
+ let encrypted = cipher.update(plaintext, 'utf8');
77
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
78
+ const tag = cipher.getAuthTag();
79
+
80
+ // Decrypt with same AAD
81
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
82
+ decipher.setAuthTag(tag);
83
+ decipher.setAAD(aad);
84
+ let decrypted = decipher.update(encrypted);
85
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
86
+ expect(decrypted.toString('utf8')).toBe(plaintext);
87
+ });
88
+
89
+ await it('should fail with wrong AAD', async () => {
90
+ const key = Buffer.alloc(32, 'k');
91
+ const iv = Buffer.alloc(12, 'i');
92
+ const plaintext = 'test';
93
+ const aad = Buffer.from('original AAD');
94
+
95
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
96
+ cipher.setAAD(aad);
97
+ let encrypted = cipher.update(plaintext, 'utf8');
98
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
99
+ const tag = cipher.getAuthTag();
100
+
101
+ // Decrypt with different AAD
102
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
103
+ decipher.setAuthTag(tag);
104
+ decipher.setAAD(Buffer.from('tampered AAD'));
105
+ decipher.update(encrypted);
106
+ expect(() => decipher.final()).toThrow();
107
+ });
108
+
109
+ await it('should handle empty plaintext', async () => {
110
+ const key = Buffer.alloc(32, 'k');
111
+ const iv = Buffer.alloc(12, 'i');
112
+
113
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
114
+ const encrypted = cipher.final();
115
+ const tag = cipher.getAuthTag();
116
+
117
+ expect(encrypted.length).toBe(0);
118
+ expect(tag.length).toBe(16);
119
+
120
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
121
+ decipher.setAuthTag(tag);
122
+ const decrypted = decipher.final();
123
+ expect(decrypted.length).toBe(0);
124
+ });
125
+
126
+ await it('should produce different ciphertext for different IVs', async () => {
127
+ const key = Buffer.alloc(32, 'k');
128
+ const plaintext = 'same data';
129
+
130
+ const cipher1 = createCipheriv('aes-256-gcm', key, Buffer.alloc(12, 1));
131
+ let enc1 = cipher1.update(plaintext, 'utf8');
132
+ enc1 = Buffer.concat([enc1, cipher1.final()]);
133
+
134
+ const cipher2 = createCipheriv('aes-256-gcm', key, Buffer.alloc(12, 2));
135
+ let enc2 = cipher2.update(plaintext, 'utf8');
136
+ enc2 = Buffer.concat([enc2, cipher2.final()]);
137
+
138
+ expect(enc1.toString('hex')).not.toBe(enc2.toString('hex'));
139
+ });
140
+ });
141
+ };
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { createHash, getHashes } from 'node:crypto';
3
+
4
+ export default async () => {
5
+ await describe('crypto.createHash', async () => {
6
+ await it('should hash empty string with sha256', async () => {
7
+ const hash = createHash('sha256').update('').digest('hex');
8
+ expect(hash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
9
+ });
10
+
11
+ await it('should hash "hello" with sha256', async () => {
12
+ const hash = createHash('sha256').update('hello').digest('hex');
13
+ expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
14
+ });
15
+
16
+ await it('should hash "hello" with md5', async () => {
17
+ const hash = createHash('md5').update('hello').digest('hex');
18
+ expect(hash).toBe('5d41402abc4b2a76b9719d911017c592');
19
+ });
20
+
21
+ await it('should hash "hello" with sha1', async () => {
22
+ const hash = createHash('sha1').update('hello').digest('hex');
23
+ expect(hash).toBe('aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d');
24
+ });
25
+
26
+ await it('should hash "hello" with sha512', async () => {
27
+ const hash = createHash('sha512').update('hello').digest('hex');
28
+ expect(hash).toBe('9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043');
29
+ });
30
+
31
+ await it('should support multiple update calls', async () => {
32
+ const hash = createHash('sha256')
33
+ .update('hello')
34
+ .update(' ')
35
+ .update('world')
36
+ .digest('hex');
37
+ const expected = createHash('sha256').update('hello world').digest('hex');
38
+ expect(hash).toBe(expected);
39
+ });
40
+
41
+ await it('should return Buffer when no encoding specified', async () => {
42
+ const result = createHash('md5').update('test').digest();
43
+ expect(result).toBeDefined();
44
+ expect(result.length).toBe(16);
45
+ });
46
+
47
+ await it('should support base64 encoding', async () => {
48
+ const hash = createHash('sha256').update('hello').digest('base64');
49
+ expect(typeof hash).toBe('string');
50
+ expect(hash.length).toBeGreaterThan(0);
51
+ });
52
+
53
+ await it('should throw on unknown algorithm', async () => {
54
+ let threw = false;
55
+ try {
56
+ createHash('unknown');
57
+ } catch (e: any) {
58
+ threw = true;
59
+ // Node.js may use different error formats; just verify it throws
60
+ }
61
+ expect(threw).toBe(true);
62
+ });
63
+
64
+ await it('should throw when digest called twice', async () => {
65
+ const hash = createHash('sha256').update('test');
66
+ hash.digest();
67
+ let threw = false;
68
+ try {
69
+ hash.digest();
70
+ } catch {
71
+ threw = true;
72
+ }
73
+ expect(threw).toBe(true);
74
+ });
75
+ });
76
+
77
+ await describe('crypto.getHashes', async () => {
78
+ await it('should return an array of supported algorithms', async () => {
79
+ const hashes = getHashes();
80
+ expect(hashes).toContain('sha256');
81
+ expect(hashes).toContain('md5');
82
+ expect(hashes).toContain('sha1');
83
+ expect(hashes).toContain('sha512');
84
+ });
85
+ });
86
+ };
package/src/hash.ts ADDED
@@ -0,0 +1,119 @@
1
+ // Reference: Node.js lib/internal/crypto/hash.js
2
+ // Reimplemented for GJS using GLib.Checksum
3
+
4
+ import GLib from '@girs/glib-2.0';
5
+ import { Transform } from 'node:stream';
6
+ import type { TransformCallback } from 'node:stream';
7
+ import { Buffer } from 'node:buffer';
8
+ import { normalizeEncoding } from '@gjsify/utils';
9
+ import { normalizeAlgorithm } from './crypto-utils.js';
10
+
11
+ const CHECKSUM_TYPES: Record<string, GLib.ChecksumType> = {
12
+ md5: GLib.ChecksumType.MD5,
13
+ sha1: GLib.ChecksumType.SHA1,
14
+ sha256: GLib.ChecksumType.SHA256,
15
+ sha384: GLib.ChecksumType.SHA384,
16
+ sha512: GLib.ChecksumType.SHA512,
17
+ };
18
+
19
+ function getChecksumType(algorithm: string): GLib.ChecksumType {
20
+ const normalized = normalizeAlgorithm(algorithm);
21
+ const type = CHECKSUM_TYPES[normalized];
22
+ if (type === undefined) {
23
+ const err = new Error(`Unknown message digest: ${algorithm}`);
24
+ (err as any).code = 'ERR_CRYPTO_HASH_UNKNOWN';
25
+ throw err;
26
+ }
27
+ return type;
28
+ }
29
+
30
+ /**
31
+ * Creates and returns a Hash object that can be used to generate hash digests
32
+ * using the given algorithm.
33
+ */
34
+ export class Hash extends Transform {
35
+ private _algorithm: string;
36
+ private _checksum: GLib.Checksum;
37
+ private _finalized = false;
38
+
39
+ constructor(algorithm: string) {
40
+ super();
41
+ const normalized = normalizeAlgorithm(algorithm);
42
+ const type = getChecksumType(normalized);
43
+ this._algorithm = normalized;
44
+ this._checksum = new GLib.Checksum(type);
45
+ }
46
+
47
+ /** Update the hash with data. */
48
+ update(data: string | Buffer | Uint8Array, inputEncoding?: BufferEncoding): this {
49
+ if (this._finalized) {
50
+ throw new Error('Digest already called');
51
+ }
52
+
53
+ let bytes: Uint8Array;
54
+ if (typeof data === 'string') {
55
+ const enc = normalizeEncoding(inputEncoding);
56
+ bytes = Buffer.from(data, enc);
57
+ } else {
58
+ bytes = data instanceof Uint8Array ? data : Buffer.from(data);
59
+ }
60
+
61
+ this._checksum.update(bytes);
62
+ return this;
63
+ }
64
+
65
+ /** Calculate the digest of all data passed to update(). */
66
+ digest(encoding?: BufferEncoding): Buffer | string {
67
+ if (this._finalized) {
68
+ throw new Error('Digest already called');
69
+ }
70
+ this._finalized = true;
71
+
72
+ const hexStr = this._checksum.get_string();
73
+ const buf = Buffer.from(hexStr, 'hex');
74
+ if (encoding) return buf.toString(encoding);
75
+ return buf;
76
+ }
77
+
78
+ /** Copy this hash to a new Hash object. */
79
+ copy(): Hash {
80
+ if (this._finalized) {
81
+ throw new Error('Digest already called');
82
+ }
83
+ const copy = Object.create(Hash.prototype) as Hash;
84
+ Transform.call(copy);
85
+ copy._algorithm = this._algorithm;
86
+ copy._finalized = false;
87
+ copy._checksum = this._checksum.copy();
88
+ return copy;
89
+ }
90
+
91
+ // Transform stream interface
92
+ _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
93
+ try {
94
+ this.update(chunk, encoding);
95
+ callback();
96
+ } catch (err) {
97
+ callback(err as Error);
98
+ }
99
+ }
100
+
101
+ _flush(callback: TransformCallback): void {
102
+ try {
103
+ this.push(this.digest());
104
+ callback();
105
+ } catch (err) {
106
+ callback(err as Error);
107
+ }
108
+ }
109
+ }
110
+
111
+ /** Get the list of supported hash algorithms. */
112
+ export function getHashes(): string[] {
113
+ return ['md5', 'sha1', 'sha256', 'sha384', 'sha512'];
114
+ }
115
+
116
+ /** Convenience: one-shot hash (Node 21+). */
117
+ export function hash(algorithm: string, data: string | Buffer | Uint8Array, encoding?: BufferEncoding): Buffer | string {
118
+ return new Hash(algorithm).update(data).digest(encoding);
119
+ }
package/src/hkdf.ts ADDED
@@ -0,0 +1,99 @@
1
+ // Reference: Node.js lib/internal/crypto/hkdf.js, RFC 5869
2
+ // Reimplemented for GJS using pure-JS Hmac
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) {
9
+ const hmac = new Hmac(algo, key);
10
+ hmac.update(data);
11
+ return hmac.digest() as Buffer;
12
+ }
13
+
14
+ /**
15
+ * HKDF-Extract: PRK = HMAC-Hash(salt, IKM)
16
+ */
17
+ function hkdfExtract(algo: string, ikm: Buffer, salt: Buffer): Buffer {
18
+ return hmacDigest(algo, salt, ikm);
19
+ }
20
+
21
+ /**
22
+ * HKDF-Expand: OKM = T(1) || T(2) || ... || T(N)
23
+ * T(i) = HMAC-Hash(PRK, T(i-1) || info || i)
24
+ */
25
+ function hkdfExpand(algo: string, prk: Buffer, info: Buffer, length: number, hashLen: number): Buffer {
26
+ const n = Math.ceil(length / hashLen);
27
+ if (n > 255) {
28
+ throw new Error('HKDF cannot generate more than 255 * HashLen bytes');
29
+ }
30
+
31
+ const okm = Buffer.allocUnsafe(n * hashLen);
32
+ let prev: Buffer = Buffer.alloc(0);
33
+
34
+ for (let i = 1; i <= n; i++) {
35
+ const input = Buffer.concat([prev, info, Buffer.from([i])]);
36
+ prev = hmacDigest(algo, prk, input);
37
+ prev.copy(okm, (i - 1) * hashLen);
38
+ }
39
+
40
+ return Buffer.from(okm.buffer, okm.byteOffset, length);
41
+ }
42
+
43
+ /**
44
+ * Synchronous HKDF key derivation.
45
+ */
46
+ export function hkdfSync(
47
+ digest: string,
48
+ ikm: string | Buffer | Uint8Array | DataView | ArrayBuffer,
49
+ salt: string | Buffer | Uint8Array | DataView | ArrayBuffer,
50
+ info: string | Buffer | Uint8Array | DataView | ArrayBuffer,
51
+ keylen: number
52
+ ): ArrayBuffer {
53
+ const algo = normalizeAlgorithm(digest);
54
+ const hashLen = DIGEST_SIZES[algo];
55
+
56
+ if (!SUPPORTED_ALGORITHMS.has(algo) || hashLen === undefined) {
57
+ throw new TypeError(`Unknown message digest: ${digest}`);
58
+ }
59
+
60
+ if (typeof keylen !== 'number' || keylen < 0) {
61
+ throw new TypeError('keylen must be a non-negative number');
62
+ }
63
+
64
+ const ikmBuf = toBuffer(ikm);
65
+ const saltBuf = toBuffer(salt);
66
+ const infoBuf = toBuffer(info);
67
+
68
+ // If salt is empty, use a zero-filled buffer of hash length
69
+ const effectiveSalt = saltBuf.length === 0 ? Buffer.alloc(hashLen, 0) : saltBuf;
70
+
71
+ const prk = hkdfExtract(algo, ikmBuf, effectiveSalt);
72
+ const okm = hkdfExpand(algo, prk, infoBuf, keylen, hashLen);
73
+
74
+ // Node.js returns ArrayBuffer — copy to ensure a clean ArrayBuffer
75
+ const result = new ArrayBuffer(okm.length);
76
+ new Uint8Array(result).set(okm);
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Asynchronous HKDF key derivation.
82
+ */
83
+ export function hkdf(
84
+ digest: string,
85
+ ikm: string | Buffer | Uint8Array | DataView | ArrayBuffer,
86
+ salt: string | Buffer | Uint8Array | DataView | ArrayBuffer,
87
+ info: string | Buffer | Uint8Array | DataView | ArrayBuffer,
88
+ keylen: number,
89
+ callback: (err: Error | null, derivedKey?: ArrayBuffer) => void
90
+ ): void {
91
+ setTimeout(() => {
92
+ try {
93
+ const result = hkdfSync(digest, ikm, salt, info, keylen);
94
+ callback(null, result);
95
+ } catch (err) {
96
+ callback(err instanceof Error ? err : new Error(String(err)));
97
+ }
98
+ }, 0);
99
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { createHmac } from 'node:crypto';
3
+ import { Buffer } from 'node:buffer';
4
+
5
+ // Ported from refs/node/test/parallel/test-crypto-hmac.js
6
+ // Original: MIT license, Node.js contributors
7
+
8
+ export default async () => {
9
+ await describe('crypto.createHmac', async () => {
10
+ await it('should create hmac with sha256', async () => {
11
+ const hmac = createHmac('sha256', 'secret').update('hello').digest('hex');
12
+ expect(typeof hmac).toBe('string');
13
+ expect(hmac.length).toBe(64); // sha256 hex = 64 chars
14
+ });
15
+
16
+ await it('should create hmac with sha1', async () => {
17
+ const hmac = createHmac('sha1', 'secret').update('hello').digest('hex');
18
+ expect(typeof hmac).toBe('string');
19
+ expect(hmac.length).toBe(40); // sha1 hex = 40 chars
20
+ });
21
+
22
+ await it('should create hmac with md5', async () => {
23
+ const hmac = createHmac('md5', 'secret').update('hello').digest('hex');
24
+ expect(typeof hmac).toBe('string');
25
+ expect(hmac.length).toBe(32); // md5 hex = 32 chars
26
+ });
27
+
28
+ await it('should support Buffer key', async () => {
29
+ const key = Buffer.from('secret');
30
+ const hmac = createHmac('sha256', key).update('hello').digest('hex');
31
+ const hmac2 = createHmac('sha256', 'secret').update('hello').digest('hex');
32
+ expect(hmac).toBe(hmac2);
33
+ });
34
+
35
+ await it('should support multiple update calls', async () => {
36
+ const hmac1 = createHmac('sha256', 'key')
37
+ .update('hello')
38
+ .update(' ')
39
+ .update('world')
40
+ .digest('hex');
41
+ const hmac2 = createHmac('sha256', 'key')
42
+ .update('hello world')
43
+ .digest('hex');
44
+ expect(hmac1).toBe(hmac2);
45
+ });
46
+
47
+ await it('should support base64 encoding', async () => {
48
+ const hmac = createHmac('sha256', 'secret').update('test').digest('base64');
49
+ expect(typeof hmac).toBe('string');
50
+ expect(hmac.length).toBeGreaterThan(0);
51
+ });
52
+
53
+ await it('should return Buffer when no encoding', async () => {
54
+ const hmac = createHmac('sha256', 'secret').update('test').digest();
55
+ expect(hmac.length).toBe(32); // sha256 = 32 bytes
56
+ });
57
+
58
+ // RFC 2202 test vector
59
+ await it('should match HMAC-SHA256 test vector', async () => {
60
+ const hmac = createHmac('sha256', 'key').update('The quick brown fox jumps over the lazy dog').digest('hex');
61
+ expect(hmac).toBe('f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8');
62
+ });
63
+ });
64
+ };
package/src/hmac.ts ADDED
@@ -0,0 +1,123 @@
1
+ // Reference: RFC 2104, Node.js lib/internal/crypto/hash.js
2
+ // Reimplemented for GJS using pure-JS HMAC over GLib.Checksum
3
+ // (GLib.Hmac bindings crash in GJS with segfault)
4
+
5
+ import { Transform } from 'node:stream';
6
+ import type { TransformCallback } from 'node:stream';
7
+ import { Buffer } from 'node:buffer';
8
+ import { normalizeEncoding } from '@gjsify/utils';
9
+ import { Hash } from './hash.js';
10
+ import { normalizeAlgorithm, BLOCK_SIZES, SUPPORTED_ALGORITHMS } from './crypto-utils.js';
11
+
12
+ /**
13
+ * Creates and returns an Hmac object that uses the given algorithm and key.
14
+ * Implemented using createHash (GLib.Checksum) since GLib.Hmac bindings are broken in GJS.
15
+ */
16
+ export class Hmac extends Transform {
17
+ private _algorithm: string;
18
+ private _innerHash: Hash;
19
+ private _outerKeyPad: Uint8Array;
20
+ private _finalized = false;
21
+
22
+ constructor(algorithm: string, key: string | Buffer | Uint8Array) {
23
+ super();
24
+ const normalized = normalizeAlgorithm(algorithm);
25
+ if (!SUPPORTED_ALGORITHMS.has(normalized)) {
26
+ const err = new Error(`Unknown message digest: ${algorithm}`);
27
+ (err as any).code = 'ERR_CRYPTO_HASH_UNKNOWN';
28
+ throw err;
29
+ }
30
+ this._algorithm = normalized;
31
+
32
+ let keyBytes: Uint8Array;
33
+ if (typeof key === 'string') {
34
+ keyBytes = Buffer.from(key, 'utf8');
35
+ } else {
36
+ keyBytes = key instanceof Uint8Array ? key : Buffer.from(key);
37
+ }
38
+
39
+ const blockSize = BLOCK_SIZES[normalized];
40
+
41
+ // If key is longer than block size, hash it first
42
+ if (keyBytes.length > blockSize) {
43
+ const h = new Hash(normalized);
44
+ h.update(keyBytes);
45
+ keyBytes = h.digest() as Buffer;
46
+ }
47
+
48
+ // Pad key to block size
49
+ const paddedKey = new Uint8Array(blockSize);
50
+ paddedKey.set(keyBytes);
51
+
52
+ // Compute inner and outer key pads
53
+ const iKeyPad = new Uint8Array(blockSize);
54
+ const oKeyPad = new Uint8Array(blockSize);
55
+ for (let i = 0; i < blockSize; i++) {
56
+ iKeyPad[i] = paddedKey[i] ^ 0x36;
57
+ oKeyPad[i] = paddedKey[i] ^ 0x5c;
58
+ }
59
+
60
+ this._outerKeyPad = oKeyPad;
61
+
62
+ // Start inner hash: H(iKeyPad || message)
63
+ this._innerHash = new Hash(normalized);
64
+ this._innerHash.update(iKeyPad);
65
+ }
66
+
67
+ /** Update the HMAC with data. */
68
+ update(data: string | Buffer | Uint8Array, inputEncoding?: BufferEncoding): this {
69
+ if (this._finalized) {
70
+ throw new Error('Digest already called');
71
+ }
72
+
73
+ let bytes: Uint8Array;
74
+ if (typeof data === 'string') {
75
+ const enc = normalizeEncoding(inputEncoding);
76
+ bytes = Buffer.from(data, enc);
77
+ } else {
78
+ bytes = data instanceof Uint8Array ? data : Buffer.from(data);
79
+ }
80
+
81
+ this._innerHash.update(bytes);
82
+ return this;
83
+ }
84
+
85
+ /** Calculate the HMAC digest. */
86
+ digest(encoding?: BufferEncoding): Buffer | string {
87
+ if (this._finalized) {
88
+ throw new Error('Digest already called');
89
+ }
90
+ this._finalized = true;
91
+
92
+ // Complete inner hash
93
+ const innerDigest = this._innerHash.digest() as Buffer;
94
+
95
+ // Compute outer hash: H(oKeyPad || innerDigest)
96
+ const outerHash = new Hash(this._algorithm);
97
+ outerHash.update(this._outerKeyPad);
98
+ outerHash.update(innerDigest);
99
+
100
+ const result = outerHash.digest() as Buffer;
101
+ if (encoding) return result.toString(encoding);
102
+ return result;
103
+ }
104
+
105
+ // Transform stream interface
106
+ _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
107
+ try {
108
+ this.update(chunk, encoding);
109
+ callback();
110
+ } catch (err) {
111
+ callback(err as Error);
112
+ }
113
+ }
114
+
115
+ _flush(callback: TransformCallback): void {
116
+ try {
117
+ this.push(this.digest());
118
+ callback();
119
+ } catch (err) {
120
+ callback(err as Error);
121
+ }
122
+ }
123
+ }