leviathan-crypto 1.3.1 → 2.0.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 (124) hide show
  1. package/CLAUDE.md +129 -76
  2. package/README.md +166 -221
  3. package/SECURITY.md +89 -37
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +78 -0
  6. package/dist/chacha20/embedded.d.ts +1 -0
  7. package/dist/chacha20/embedded.js +27 -0
  8. package/dist/chacha20/index.d.ts +20 -7
  9. package/dist/chacha20/index.js +41 -14
  10. package/dist/chacha20/ops.d.ts +1 -1
  11. package/dist/chacha20/ops.js +19 -18
  12. package/dist/chacha20/pool-worker.js +77 -0
  13. package/dist/ct-wasm.d.ts +1 -0
  14. package/dist/ct-wasm.js +3 -0
  15. package/dist/ct.wasm +0 -0
  16. package/dist/docs/aead.md +320 -0
  17. package/dist/docs/architecture.md +419 -285
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +218 -150
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +65 -74
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -132
  24. package/dist/docs/serpent.md +134 -565
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +93 -16
  28. package/dist/docs/utils.md +114 -41
  29. package/dist/embedded/chacha20.d.ts +1 -1
  30. package/dist/embedded/chacha20.js +2 -1
  31. package/dist/embedded/kyber.d.ts +1 -0
  32. package/dist/embedded/kyber.js +3 -0
  33. package/dist/embedded/serpent.d.ts +1 -1
  34. package/dist/embedded/serpent.js +2 -1
  35. package/dist/embedded/sha2.d.ts +1 -1
  36. package/dist/embedded/sha2.js +2 -1
  37. package/dist/embedded/sha3.d.ts +1 -1
  38. package/dist/embedded/sha3.js +2 -1
  39. package/dist/errors.d.ts +10 -0
  40. package/dist/{serpent/seal.js → errors.js} +14 -46
  41. package/dist/fortuna.d.ts +2 -8
  42. package/dist/fortuna.js +11 -9
  43. package/dist/index.d.ts +25 -9
  44. package/dist/index.js +36 -7
  45. package/dist/init.d.ts +3 -7
  46. package/dist/init.js +18 -35
  47. package/dist/keccak/embedded.d.ts +1 -0
  48. package/dist/keccak/embedded.js +27 -0
  49. package/dist/keccak/index.d.ts +4 -0
  50. package/dist/keccak/index.js +31 -0
  51. package/dist/kyber/embedded.d.ts +1 -0
  52. package/dist/kyber/embedded.js +27 -0
  53. package/dist/kyber/indcpa.d.ts +49 -0
  54. package/dist/kyber/indcpa.js +352 -0
  55. package/dist/kyber/index.d.ts +38 -0
  56. package/dist/kyber/index.js +150 -0
  57. package/dist/kyber/kem.d.ts +21 -0
  58. package/dist/kyber/kem.js +160 -0
  59. package/dist/kyber/params.d.ts +14 -0
  60. package/dist/kyber/params.js +37 -0
  61. package/dist/kyber/suite.d.ts +13 -0
  62. package/dist/kyber/suite.js +93 -0
  63. package/dist/kyber/types.d.ts +98 -0
  64. package/dist/kyber/types.js +25 -0
  65. package/dist/kyber/validate.d.ts +19 -0
  66. package/dist/kyber/validate.js +68 -0
  67. package/dist/kyber.wasm +0 -0
  68. package/dist/loader.d.ts +19 -4
  69. package/dist/loader.js +91 -25
  70. package/dist/serpent/cipher-suite.d.ts +4 -0
  71. package/dist/serpent/cipher-suite.js +121 -0
  72. package/dist/serpent/embedded.d.ts +1 -0
  73. package/dist/serpent/embedded.js +27 -0
  74. package/dist/serpent/index.d.ts +6 -37
  75. package/dist/serpent/index.js +9 -118
  76. package/dist/serpent/pool-worker.d.ts +1 -0
  77. package/dist/serpent/pool-worker.js +202 -0
  78. package/dist/serpent/serpent-cbc.d.ts +30 -0
  79. package/dist/serpent/serpent-cbc.js +136 -0
  80. package/dist/sha2/embedded.d.ts +1 -0
  81. package/dist/sha2/embedded.js +27 -0
  82. package/dist/sha2/hkdf.js +6 -2
  83. package/dist/sha2/index.d.ts +3 -2
  84. package/dist/sha2/index.js +3 -4
  85. package/dist/sha3/embedded.d.ts +1 -0
  86. package/dist/sha3/embedded.js +27 -0
  87. package/dist/sha3/index.d.ts +3 -2
  88. package/dist/sha3/index.js +3 -4
  89. package/dist/stream/constants.d.ts +6 -0
  90. package/dist/stream/constants.js +30 -0
  91. package/dist/stream/header.d.ts +9 -0
  92. package/dist/stream/header.js +77 -0
  93. package/dist/stream/index.d.ts +7 -0
  94. package/dist/stream/index.js +27 -0
  95. package/dist/stream/open-stream.d.ts +21 -0
  96. package/dist/stream/open-stream.js +146 -0
  97. package/dist/stream/seal-stream-pool.d.ts +38 -0
  98. package/dist/stream/seal-stream-pool.js +391 -0
  99. package/dist/stream/seal-stream.d.ts +20 -0
  100. package/dist/stream/seal-stream.js +142 -0
  101. package/dist/stream/seal.d.ts +9 -0
  102. package/dist/stream/seal.js +75 -0
  103. package/dist/stream/types.d.ts +24 -0
  104. package/dist/stream/types.js +26 -0
  105. package/dist/utils.d.ts +12 -7
  106. package/dist/utils.js +75 -19
  107. package/dist/wasm-source.d.ts +12 -0
  108. package/dist/wasm-source.js +26 -0
  109. package/package.json +13 -5
  110. package/dist/chacha20/pool.d.ts +0 -52
  111. package/dist/chacha20/pool.js +0 -188
  112. package/dist/chacha20/pool.worker.js +0 -37
  113. package/dist/docs/chacha20_pool.md +0 -309
  114. package/dist/docs/wasm.md +0 -194
  115. package/dist/serpent/seal.d.ts +0 -8
  116. package/dist/serpent/stream-pool.d.ts +0 -48
  117. package/dist/serpent/stream-pool.js +0 -285
  118. package/dist/serpent/stream-sealer.d.ts +0 -50
  119. package/dist/serpent/stream-sealer.js +0 -341
  120. package/dist/serpent/stream.d.ts +0 -28
  121. package/dist/serpent/stream.js +0 -205
  122. package/dist/serpent/stream.worker.d.ts +0 -32
  123. package/dist/serpent/stream.worker.js +0 -117
  124. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
@@ -0,0 +1,202 @@
1
+ // / <reference lib="webworker" />
2
+ // src/ts/serpent/pool-worker.ts
3
+ //
4
+ // Worker for SealStreamPool with SerpentCipher.
5
+ // Holds 3 derived keys (enc/mac/iv) and raw WASM instances.
6
+ // Direct WASM calls — no initModule (avoids same-thread module cache conflicts
7
+ // in @vitest/web-worker test environment).
8
+ import { constantTimeEqual, wipe, concat } from '../utils.js';
9
+ import { AuthenticationError } from '../errors.js';
10
+ let sha2;
11
+ let serpent;
12
+ let keys;
13
+ function hmacSha256(key, msg) {
14
+ const x = sha2;
15
+ let k = key;
16
+ if (k.length > 64) {
17
+ x.sha256Init();
18
+ feedSha2(k);
19
+ x.sha256Final();
20
+ const mem = new Uint8Array(x.memory.buffer);
21
+ k = mem.slice(x.getSha256OutOffset(), x.getSha256OutOffset() + 32);
22
+ }
23
+ const mem = new Uint8Array(x.memory.buffer);
24
+ mem.set(k, x.getSha256InputOffset());
25
+ x.hmac256Init(k.length);
26
+ feedHmac(msg);
27
+ x.hmac256Final();
28
+ return new Uint8Array(x.memory.buffer).slice(x.getSha256OutOffset(), x.getSha256OutOffset() + 32);
29
+ }
30
+ function feedSha2(data) {
31
+ const x = sha2;
32
+ const mem = new Uint8Array(x.memory.buffer);
33
+ const off = x.getSha256InputOffset();
34
+ let pos = 0;
35
+ while (pos < data.length) {
36
+ const n = Math.min(data.length - pos, 64);
37
+ mem.set(data.subarray(pos, pos + n), off);
38
+ x.sha256Update(n);
39
+ pos += n;
40
+ }
41
+ }
42
+ function feedHmac(data) {
43
+ const x = sha2;
44
+ const mem = new Uint8Array(x.memory.buffer);
45
+ const off = x.getSha256InputOffset();
46
+ let pos = 0;
47
+ while (pos < data.length) {
48
+ const n = Math.min(data.length - pos, 64);
49
+ mem.set(data.subarray(pos, pos + n), off);
50
+ x.hmac256Update(n);
51
+ pos += n;
52
+ }
53
+ }
54
+ function pkcs7Pad(data) {
55
+ const padLen = 16 - (data.length % 16);
56
+ const out = new Uint8Array(data.length + padLen);
57
+ out.set(data);
58
+ out.fill(padLen, data.length);
59
+ return out;
60
+ }
61
+ // pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
62
+ // The early throw on invalid padLen is not a padding oracle in this context —
63
+ // the HMAC check is the oracle gate and runs in constant time before this point.
64
+ // If you move this call to a pre-auth site, revisit the timing properties.
65
+ function pkcs7Strip(data) {
66
+ if (data.length === 0)
67
+ throw new RangeError('empty ciphertext');
68
+ const padLen = data[data.length - 1];
69
+ if (padLen === 0 || padLen > 16)
70
+ throw new RangeError('invalid PKCS7 padding');
71
+ if (padLen > data.length)
72
+ throw new RangeError('invalid PKCS7 padding');
73
+ let bad = 0;
74
+ for (let i = data.length - padLen; i < data.length; i++)
75
+ bad |= data[i] ^ padLen;
76
+ if (bad !== 0)
77
+ throw new RangeError('invalid PKCS7 padding');
78
+ return data.subarray(0, data.length - padLen);
79
+ }
80
+ function cbcEncrypt(encKey, iv, plaintext) {
81
+ const s = serpent;
82
+ const mem = new Uint8Array(s.memory.buffer);
83
+ mem.set(encKey, s.getKeyOffset());
84
+ s.loadKey(encKey.length);
85
+ mem.set(iv, s.getCbcIvOffset());
86
+ const padded = pkcs7Pad(plaintext);
87
+ mem.set(padded, s.getChunkPtOffset());
88
+ s.cbcEncryptChunk(padded.length);
89
+ return new Uint8Array(s.memory.buffer).slice(s.getChunkCtOffset(), s.getChunkCtOffset() + padded.length);
90
+ }
91
+ function cbcDecrypt(encKey, iv, ct) {
92
+ const s = serpent;
93
+ const mem = new Uint8Array(s.memory.buffer);
94
+ mem.set(encKey, s.getKeyOffset());
95
+ s.loadKey(encKey.length);
96
+ mem.set(iv, s.getCbcIvOffset());
97
+ mem.set(ct, s.getChunkCtOffset());
98
+ s.cbcDecryptChunk(ct.length);
99
+ const raw = new Uint8Array(s.memory.buffer).slice(s.getChunkPtOffset(), s.getChunkPtOffset() + ct.length);
100
+ return pkcs7Strip(raw);
101
+ }
102
+ self.onmessage = async (e) => {
103
+ const msg = e.data;
104
+ if (msg.type === 'init') {
105
+ try {
106
+ const sha2Mem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
107
+ const sha2Inst = await WebAssembly.instantiate(msg.modules.sha2, { env: { memory: sha2Mem } });
108
+ sha2 = sha2Inst.exports;
109
+ const serpentMem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
110
+ const serpentInst = await WebAssembly.instantiate(msg.modules.serpent, { env: { memory: serpentMem } });
111
+ serpent = serpentInst.exports;
112
+ keys = new Uint8Array(msg.derivedKeyBytes);
113
+ if (keys.length !== 96)
114
+ throw new Error(`expected 96 derived key bytes (got ${keys.length})`);
115
+ msg.derivedKeyBytes.fill(0);
116
+ self.postMessage({ type: 'ready' });
117
+ }
118
+ catch (err) {
119
+ self.postMessage({ type: 'error', id: -1, message: err.message, isAuthError: false });
120
+ }
121
+ return;
122
+ }
123
+ if (msg.type === 'wipe') {
124
+ if (keys)
125
+ keys.fill(0);
126
+ keys = undefined;
127
+ if (sha2)
128
+ sha2.wipeBuffers();
129
+ if (serpent)
130
+ serpent.wipeBuffers();
131
+ sha2 = undefined;
132
+ serpent = undefined;
133
+ return;
134
+ }
135
+ if (!keys || !sha2 || !serpent) {
136
+ self.postMessage({ type: 'error', id: msg.id, message: 'worker not initialized', isAuthError: false });
137
+ return;
138
+ }
139
+ try {
140
+ const { id, op, counterNonce, data, aad } = msg;
141
+ const aadBytes = aad ?? new Uint8Array(0);
142
+ const jobKey = msg.derivedKeyBytes ?? keys;
143
+ const encKey = jobKey.subarray(0, 32);
144
+ const macKey = jobKey.subarray(32, 64);
145
+ const ivKey = jobKey.subarray(64, 96);
146
+ let result;
147
+ if (op === 'seal') {
148
+ const ivFull = hmacSha256(ivKey, counterNonce);
149
+ const iv = ivFull.slice(0, 16);
150
+ wipe(ivFull);
151
+ const ct = cbcEncrypt(encKey, iv, data);
152
+ const aadLenBuf = new Uint8Array(4);
153
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
154
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
155
+ const tag = hmacSha256(macKey, tagInput);
156
+ result = concat(ct, tag);
157
+ wipe(iv);
158
+ wipe(tagInput);
159
+ }
160
+ else {
161
+ const ct = data.subarray(0, data.length - 32);
162
+ const receivedTag = data.subarray(data.length - 32);
163
+ const ivFull = hmacSha256(ivKey, counterNonce);
164
+ const iv = ivFull.slice(0, 16);
165
+ wipe(ivFull);
166
+ const aadLenBuf = new Uint8Array(4);
167
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
168
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
169
+ const expectedTag = hmacSha256(macKey, tagInput);
170
+ // CRITICAL: verify HMAC before decrypting (Vaudenay 2002)
171
+ if (!constantTimeEqual(expectedTag, receivedTag)) {
172
+ wipe(iv);
173
+ wipe(tagInput);
174
+ wipe(expectedTag);
175
+ throw new AuthenticationError('serpent');
176
+ }
177
+ wipe(tagInput);
178
+ wipe(expectedTag);
179
+ result = cbcDecrypt(encKey, iv, ct);
180
+ wipe(iv);
181
+ }
182
+ const transfer = result.buffer instanceof ArrayBuffer ? [result.buffer] : [];
183
+ self.postMessage({ type: 'result', id, data: result }, { transfer });
184
+ }
185
+ catch (err) {
186
+ const isAuth = err instanceof AuthenticationError;
187
+ self.postMessage({
188
+ type: 'error', id: msg.id,
189
+ message: err.message,
190
+ cipher: isAuth ? 'serpent' : undefined,
191
+ isAuthError: isAuth,
192
+ });
193
+ }
194
+ finally {
195
+ if (msg.derivedKeyBytes)
196
+ msg.derivedKeyBytes.fill(0);
197
+ if (sha2)
198
+ sha2.wipeBuffers();
199
+ if (serpent)
200
+ serpent.wipeBuffers();
201
+ }
202
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Serpent-256 in CBC mode with PKCS7 padding.
3
+ *
4
+ * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
5
+ * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
6
+ */
7
+ export declare class SerpentCbc {
8
+ private readonly x;
9
+ constructor(opts?: {
10
+ dangerUnauthenticated: true;
11
+ });
12
+ private get mem();
13
+ /**
14
+ * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
15
+ *
16
+ * @param key 16, 24, or 32 bytes
17
+ * @param iv 16 bytes — must be random and unique per (key, message)
18
+ * @param plaintext any length — PKCS7 padding applied automatically
19
+ * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
20
+ */
21
+ encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
22
+ /**
23
+ * Decrypt Serpent-256 CBC + PKCS7.
24
+ * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
25
+ */
26
+ decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
27
+ dispose(): void;
28
+ private _loadKey;
29
+ private _setIv;
30
+ }
@@ -0,0 +1,136 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/serpent/serpent-cbc.ts
23
+ //
24
+ // SerpentCbc — Serpent-256 CBC + PKCS7, internal module.
25
+ // Extracted to break the cipher-suite.ts ↔ index.ts circular dependency.
26
+ // Import from here directly; index.ts re-exports for the public API surface.
27
+ import { getInstance } from '../init.js';
28
+ function getExports() {
29
+ return getInstance('serpent').exports;
30
+ }
31
+ // ── PKCS7 helpers ────────────────────────────────────────────────────────────
32
+ function pkcs7Pad(data) {
33
+ const padLen = 16 - (data.length % 16); // 1..16
34
+ const out = new Uint8Array(data.length + padLen);
35
+ out.set(data);
36
+ out.fill(padLen, data.length);
37
+ return out;
38
+ }
39
+ // pkcs7Strip is only called after HMAC authentication succeeds (verify-then-decrypt).
40
+ // The early throw on invalid padLen is not a padding oracle in this context —
41
+ // the HMAC check is the oracle gate and runs in constant time before this point.
42
+ // If you move this call to a pre-auth site, revisit the timing properties.
43
+ function pkcs7Strip(data) {
44
+ if (data.length === 0)
45
+ throw new RangeError('empty ciphertext');
46
+ const padLen = data[data.length - 1];
47
+ if (padLen === 0 || padLen > 16)
48
+ throw new RangeError(`invalid PKCS7 padding byte: ${padLen}`);
49
+ if (padLen > data.length)
50
+ throw new RangeError(`invalid PKCS7 padding: pad length ${padLen} exceeds data length ${data.length}`);
51
+ let bad = 0;
52
+ for (let i = data.length - padLen; i < data.length; i++)
53
+ bad |= data[i] ^ padLen;
54
+ if (bad !== 0)
55
+ throw new RangeError('invalid PKCS7 padding');
56
+ return data.subarray(0, data.length - padLen);
57
+ }
58
+ // ── SerpentCbc ───────────────────────────────────────────────────────────────
59
+ /**
60
+ * Serpent-256 in CBC mode with PKCS7 padding.
61
+ *
62
+ * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
63
+ * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
64
+ */
65
+ export class SerpentCbc {
66
+ x;
67
+ constructor(opts) {
68
+ if (!opts?.dangerUnauthenticated) {
69
+ throw new Error('leviathan-crypto: SerpentCbc is unauthenticated — use Seal with SerpentCipher instead. ' +
70
+ 'To use SerpentCbc directly, pass { dangerUnauthenticated: true }.');
71
+ }
72
+ this.x = getExports();
73
+ }
74
+ get mem() {
75
+ return new Uint8Array(this.x.memory.buffer);
76
+ }
77
+ /**
78
+ * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
79
+ *
80
+ * @param key 16, 24, or 32 bytes
81
+ * @param iv 16 bytes — must be random and unique per (key, message)
82
+ * @param plaintext any length — PKCS7 padding applied automatically
83
+ * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
84
+ */
85
+ encrypt(key, iv, plaintext) {
86
+ this._loadKey(key);
87
+ this._setIv(iv);
88
+ const padded = pkcs7Pad(plaintext);
89
+ const output = new Uint8Array(padded.length);
90
+ const ptOff = this.x.getChunkPtOffset();
91
+ const ctOff = this.x.getChunkCtOffset();
92
+ const maxChunk = this.x.getChunkSize();
93
+ for (let off = 0; off < padded.length; off += maxChunk) {
94
+ const chunk = padded.subarray(off, Math.min(off + maxChunk, padded.length));
95
+ this.mem.set(chunk, ptOff);
96
+ this.x.cbcEncryptChunk(chunk.length);
97
+ output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
98
+ }
99
+ return output;
100
+ }
101
+ /**
102
+ * Decrypt Serpent-256 CBC + PKCS7.
103
+ * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
104
+ */
105
+ decrypt(key, iv, ciphertext) {
106
+ if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
107
+ throw new RangeError('ciphertext length must be a non-zero multiple of 16');
108
+ this._loadKey(key);
109
+ this._setIv(iv);
110
+ const output = new Uint8Array(ciphertext.length);
111
+ const ctOff = this.x.getChunkCtOffset();
112
+ const ptOff = this.x.getChunkPtOffset();
113
+ const maxChunk = this.x.getChunkSize();
114
+ for (let off = 0; off < ciphertext.length; off += maxChunk) {
115
+ const chunk = ciphertext.subarray(off, Math.min(off + maxChunk, ciphertext.length));
116
+ this.mem.set(chunk, ctOff);
117
+ this.x.cbcDecryptChunk_simd(chunk.length);
118
+ output.set(new Uint8Array(this.x.memory.buffer).subarray(ptOff, ptOff + chunk.length), off);
119
+ }
120
+ return pkcs7Strip(output);
121
+ }
122
+ dispose() {
123
+ this.x.wipeBuffers();
124
+ }
125
+ _loadKey(key) {
126
+ if (key.length !== 16 && key.length !== 24 && key.length !== 32)
127
+ throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
128
+ this.mem.set(key, this.x.getKeyOffset());
129
+ this.x.loadKey(key.length);
130
+ }
131
+ _setIv(iv) {
132
+ if (iv.length !== 16)
133
+ throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
134
+ this.mem.set(iv, this.x.getCbcIvOffset());
135
+ }
136
+ }
@@ -0,0 +1 @@
1
+ export { WASM_GZ_BASE64 as sha2Wasm } from '../embedded/sha2.js';
@@ -0,0 +1,27 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/sha2/embedded.ts
23
+ //
24
+ // Exports the gzip+base64 sha2 WASM blob for use as a WasmSource.
25
+ // This is the only file in the sha2 subpath that references the embedded blob.
26
+ // Import via `leviathan-crypto/sha2/embedded`.
27
+ export { WASM_GZ_BASE64 as sha2Wasm } from '../embedded/sha2.js';
package/dist/sha2/hkdf.js CHANGED
@@ -58,7 +58,9 @@ export class HKDF_SHA256 {
58
58
  oldPrev.fill(0);
59
59
  }
60
60
  prev.fill(0);
61
- return okm.slice(0, length);
61
+ const result = okm.slice(0, length);
62
+ okm.fill(0);
63
+ return result;
62
64
  }
63
65
  // One-shot: extract then expand
64
66
  derive(ikm, salt, info, length) {
@@ -105,7 +107,9 @@ export class HKDF_SHA512 {
105
107
  oldPrev.fill(0);
106
108
  }
107
109
  prev.fill(0);
108
- return okm.slice(0, length);
110
+ const result = okm.slice(0, length);
111
+ okm.fill(0);
112
+ return result;
109
113
  }
110
114
  // One-shot: extract then expand
111
115
  derive(ikm, salt, info, length) {
@@ -1,5 +1,6 @@
1
- import type { Mode, InitOpts } from '../init.js';
2
- export declare function sha2Init(mode?: Mode, opts?: InitOpts): Promise<void>;
1
+ import type { WasmSource } from '../wasm-source.js';
2
+ export declare function sha2Init(source: WasmSource): Promise<void>;
3
+ export type { WasmSource };
3
4
  export declare function _sha2Ready(): boolean;
4
5
  export declare class SHA256 {
5
6
  private readonly x;
@@ -22,11 +22,10 @@
22
22
  // src/ts/sha2/index.ts
23
23
  //
24
24
  // Public API classes for the SHA-2 WASM module.
25
- // Uses the init() module cache — call init('sha2') before constructing.
25
+ // Uses the init() module cache — call sha2Init(source) before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
- const _embedded = () => import('../embedded/sha2.js').then(m => m.WASM_BASE64);
28
- export async function sha2Init(mode = 'embedded', opts) {
29
- return initModule('sha2', _embedded, mode, opts);
27
+ export async function sha2Init(source) {
28
+ return initModule('sha2', source);
30
29
  }
31
30
  function getExports() {
32
31
  return getInstance('sha2').exports;
@@ -0,0 +1 @@
1
+ export { WASM_GZ_BASE64 as sha3Wasm } from '../embedded/sha3.js';
@@ -0,0 +1,27 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▄ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/sha3/embedded.ts
23
+ //
24
+ // Exports the gzip+base64 sha3 WASM blob for use as a WasmSource.
25
+ // This is the only file in the sha3 subpath that references the embedded blob.
26
+ // Import via `leviathan-crypto/sha3/embedded`.
27
+ export { WASM_GZ_BASE64 as sha3Wasm } from '../embedded/sha3.js';
@@ -1,5 +1,6 @@
1
- import type { Mode, InitOpts } from '../init.js';
2
- export declare function sha3Init(mode?: Mode, opts?: InitOpts): Promise<void>;
1
+ import type { WasmSource } from '../wasm-source.js';
2
+ export declare function sha3Init(source: WasmSource): Promise<void>;
3
+ export type { WasmSource };
3
4
  export declare function _sha3Ready(): boolean;
4
5
  export declare class SHA3_256 {
5
6
  private readonly x;
@@ -22,11 +22,10 @@
22
22
  // src/ts/sha3/index.ts
23
23
  //
24
24
  // Public API classes for the SHA-3 WASM module.
25
- // Uses the init() module cache — call init('sha3') before constructing.
25
+ // Uses the init() module cache — call sha3Init(source) before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
- const _embedded = () => import('../embedded/sha3.js').then(m => m.WASM_BASE64);
28
- export async function sha3Init(mode = 'embedded', opts) {
29
- return initModule('sha3', _embedded, mode, opts);
27
+ export async function sha3Init(source) {
28
+ return initModule('sha3', source);
30
29
  }
31
30
  function getExports() {
32
31
  return getInstance('sha3').exports;
@@ -0,0 +1,6 @@
1
+ export declare const FLAG_FRAMED = 128;
2
+ export declare const TAG_DATA = 0;
3
+ export declare const TAG_FINAL = 1;
4
+ export declare const HEADER_SIZE = 20;
5
+ export declare const CHUNK_MIN = 1024;
6
+ export declare const CHUNK_MAX = 16777215;
@@ -0,0 +1,30 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/stream/constants.ts
23
+ //
24
+ // Shared constants for the STREAM construction.
25
+ export const FLAG_FRAMED = 0x80;
26
+ export const TAG_DATA = 0x00;
27
+ export const TAG_FINAL = 0x01;
28
+ export const HEADER_SIZE = 20;
29
+ export const CHUNK_MIN = 1024;
30
+ export const CHUNK_MAX = 16_777_215; // u24 max
@@ -0,0 +1,9 @@
1
+ export declare function writeHeader(formatEnum: number, framed: boolean, nonce: Uint8Array, chunkSize: number): Uint8Array;
2
+ export declare function readHeader(header: Uint8Array): {
3
+ formatEnum: number;
4
+ framed: boolean;
5
+ nonce: Uint8Array;
6
+ chunkSize: number;
7
+ };
8
+ /** 12-byte counter nonce: 11-byte BE counter + 1-byte final flag. */
9
+ export declare function makeCounterNonce(counter: number, finalFlag: number): Uint8Array;
@@ -0,0 +1,77 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/stream/header.ts
23
+ //
24
+ // Wire format header encoding/decoding and counter nonce construction.
25
+ import { FLAG_FRAMED, HEADER_SIZE, TAG_DATA, TAG_FINAL } from './constants.js';
26
+ // The 16-byte nonce is a HKDF salt — not a direct cipher nonce.
27
+ // Both XChaCha20Cipher and SerpentCipher derive their actual key material
28
+ // and nonces from this value via HKDF-SHA-256. The 16-byte size is chosen
29
+ // to satisfy HChaCha20's 16-byte input requirement while also serving as a
30
+ // sufficient HKDF salt for the Serpent construction.
31
+ export function writeHeader(formatEnum, framed, nonce, chunkSize) {
32
+ if (!Number.isInteger(formatEnum) || formatEnum < 0 || formatEnum > 0x3f)
33
+ throw new RangeError(`formatEnum must be an integer in [0, 0x3f] (got ${formatEnum})`);
34
+ if (nonce.length !== 16)
35
+ throw new RangeError(`nonce must be 16 bytes (got ${nonce.length})`);
36
+ if (!Number.isInteger(chunkSize) || chunkSize < 0 || chunkSize > 0xffffff)
37
+ throw new RangeError(`chunkSize must be an integer in [0, 0xFFFFFF] (got ${chunkSize})`);
38
+ const h = new Uint8Array(HEADER_SIZE);
39
+ h[0] = (framed ? FLAG_FRAMED : 0) | formatEnum;
40
+ h.set(nonce, 1);
41
+ // u24 big-endian chunk size
42
+ h[17] = (chunkSize >> 16) & 0xff;
43
+ h[18] = (chunkSize >> 8) & 0xff;
44
+ h[19] = chunkSize & 0xff;
45
+ return h;
46
+ }
47
+ export function readHeader(header) {
48
+ if (header.length !== HEADER_SIZE)
49
+ throw new RangeError(`header must be exactly ${HEADER_SIZE} bytes (got ${header.length})`);
50
+ const byte0 = header[0];
51
+ if (byte0 & 0x40)
52
+ throw new RangeError(`header has reserved bit 6 set (byte0=0x${byte0.toString(16).padStart(2, '0')}) — unknown or malformed wire format`);
53
+ return {
54
+ formatEnum: byte0 & 0x3f,
55
+ framed: !!(byte0 & FLAG_FRAMED),
56
+ nonce: header.slice(1, 17),
57
+ chunkSize: (header[17] << 16) | (header[18] << 8) | header[19],
58
+ };
59
+ }
60
+ /** 12-byte counter nonce: 11-byte BE counter + 1-byte final flag. */
61
+ export function makeCounterNonce(counter, finalFlag) {
62
+ if (!Number.isInteger(counter) || counter < 0 || counter > Number.MAX_SAFE_INTEGER)
63
+ throw new RangeError(`counter must be an integer in [0, ${Number.MAX_SAFE_INTEGER}]`);
64
+ if (finalFlag !== TAG_DATA && finalFlag !== TAG_FINAL)
65
+ throw new RangeError(`finalFlag must be TAG_DATA (0x00) or TAG_FINAL (0x01) (got 0x${finalFlag.toString(16).padStart(2, '0')})`);
66
+ const n = new Uint8Array(12);
67
+ // Write counter as 11-byte big-endian.
68
+ // JS safe integers fit in 53 bits — we only need the lower 53 bits.
69
+ // Pack from the right (byte 10 down to byte 0).
70
+ let c = counter;
71
+ for (let i = 10; i >= 0; i--) {
72
+ n[i] = c & 0xff;
73
+ c = Math.floor(c / 256);
74
+ }
75
+ n[11] = finalFlag;
76
+ return n;
77
+ }
@@ -0,0 +1,7 @@
1
+ export { SealStream } from './seal-stream.js';
2
+ export { OpenStream } from './open-stream.js';
3
+ export { Seal } from './seal.js';
4
+ export type { CipherSuite, DerivedKeys, SealStreamOpts } from './types.js';
5
+ export { FLAG_FRAMED, TAG_DATA, TAG_FINAL, HEADER_SIZE, CHUNK_MIN, CHUNK_MAX, } from './constants.js';
6
+ export { SealStreamPool } from './seal-stream-pool.js';
7
+ export type { PoolOpts } from './seal-stream-pool.js';