leviathan-crypto 1.4.0 → 2.0.1

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 (122) hide show
  1. package/CLAUDE.md +129 -94
  2. package/README.md +166 -223
  3. package/SECURITY.md +90 -45
  4. package/dist/chacha20/cipher-suite.d.ts +4 -0
  5. package/dist/chacha20/cipher-suite.js +79 -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 -27
  9. package/dist/chacha20/index.js +40 -59
  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 +323 -0
  17. package/dist/docs/architecture.md +427 -292
  18. package/dist/docs/argon2id.md +42 -30
  19. package/dist/docs/chacha20.md +192 -266
  20. package/dist/docs/exports.md +241 -0
  21. package/dist/docs/fortuna.md +60 -69
  22. package/dist/docs/init.md +172 -178
  23. package/dist/docs/loader.md +87 -142
  24. package/dist/docs/serpent.md +134 -583
  25. package/dist/docs/sha2.md +91 -103
  26. package/dist/docs/sha3.md +70 -36
  27. package/dist/docs/types.md +94 -16
  28. package/dist/docs/utils.md +109 -32
  29. package/dist/embedded/kyber.d.ts +1 -0
  30. package/dist/embedded/kyber.js +3 -0
  31. package/dist/embedded/serpent.d.ts +1 -1
  32. package/dist/embedded/serpent.js +1 -1
  33. package/dist/errors.d.ts +10 -0
  34. package/dist/errors.js +38 -0
  35. package/dist/fortuna.d.ts +0 -6
  36. package/dist/fortuna.js +5 -5
  37. package/dist/index.d.ts +25 -9
  38. package/dist/index.js +36 -7
  39. package/dist/init.d.ts +3 -7
  40. package/dist/init.js +18 -35
  41. package/dist/keccak/embedded.d.ts +1 -0
  42. package/dist/keccak/embedded.js +27 -0
  43. package/dist/keccak/index.d.ts +4 -0
  44. package/dist/keccak/index.js +31 -0
  45. package/dist/kyber/embedded.d.ts +1 -0
  46. package/dist/kyber/embedded.js +27 -0
  47. package/dist/kyber/indcpa.d.ts +49 -0
  48. package/dist/kyber/indcpa.js +352 -0
  49. package/dist/kyber/index.d.ts +38 -0
  50. package/dist/kyber/index.js +150 -0
  51. package/dist/kyber/kem.d.ts +21 -0
  52. package/dist/kyber/kem.js +160 -0
  53. package/dist/kyber/params.d.ts +14 -0
  54. package/dist/kyber/params.js +37 -0
  55. package/dist/kyber/suite.d.ts +13 -0
  56. package/dist/kyber/suite.js +94 -0
  57. package/dist/kyber/types.d.ts +98 -0
  58. package/dist/kyber/types.js +25 -0
  59. package/dist/kyber/validate.d.ts +19 -0
  60. package/dist/kyber/validate.js +68 -0
  61. package/dist/kyber.wasm +0 -0
  62. package/dist/loader.d.ts +15 -6
  63. package/dist/loader.js +65 -21
  64. package/dist/serpent/cipher-suite.d.ts +4 -0
  65. package/dist/serpent/cipher-suite.js +122 -0
  66. package/dist/serpent/embedded.d.ts +1 -0
  67. package/dist/serpent/embedded.js +27 -0
  68. package/dist/serpent/index.d.ts +6 -37
  69. package/dist/serpent/index.js +9 -118
  70. package/dist/serpent/pool-worker.d.ts +1 -0
  71. package/dist/serpent/pool-worker.js +208 -0
  72. package/dist/serpent/serpent-cbc.d.ts +30 -0
  73. package/dist/serpent/serpent-cbc.js +142 -0
  74. package/dist/serpent.wasm +0 -0
  75. package/dist/sha2/embedded.d.ts +1 -0
  76. package/dist/sha2/embedded.js +27 -0
  77. package/dist/sha2/hkdf.js +6 -2
  78. package/dist/sha2/index.d.ts +3 -2
  79. package/dist/sha2/index.js +3 -4
  80. package/dist/sha3/embedded.d.ts +1 -0
  81. package/dist/sha3/embedded.js +27 -0
  82. package/dist/sha3/index.d.ts +3 -2
  83. package/dist/sha3/index.js +3 -4
  84. package/dist/stream/constants.d.ts +6 -0
  85. package/dist/stream/constants.js +30 -0
  86. package/dist/stream/header.d.ts +9 -0
  87. package/dist/stream/header.js +77 -0
  88. package/dist/stream/index.d.ts +7 -0
  89. package/dist/stream/index.js +27 -0
  90. package/dist/stream/open-stream.d.ts +21 -0
  91. package/dist/stream/open-stream.js +146 -0
  92. package/dist/stream/seal-stream-pool.d.ts +38 -0
  93. package/dist/stream/seal-stream-pool.js +400 -0
  94. package/dist/stream/seal-stream.d.ts +20 -0
  95. package/dist/stream/seal-stream.js +142 -0
  96. package/dist/stream/seal.d.ts +9 -0
  97. package/dist/stream/seal.js +75 -0
  98. package/dist/stream/types.d.ts +25 -0
  99. package/dist/stream/types.js +26 -0
  100. package/dist/utils.d.ts +7 -2
  101. package/dist/utils.js +49 -3
  102. package/dist/wasm-source.d.ts +12 -0
  103. package/dist/wasm-source.js +26 -0
  104. package/package.json +13 -5
  105. package/dist/chacha20/pool.d.ts +0 -52
  106. package/dist/chacha20/pool.js +0 -178
  107. package/dist/chacha20/pool.worker.js +0 -37
  108. package/dist/chacha20/stream-sealer.d.ts +0 -49
  109. package/dist/chacha20/stream-sealer.js +0 -327
  110. package/dist/docs/chacha20_pool.md +0 -309
  111. package/dist/docs/wasm.md +0 -194
  112. package/dist/serpent/seal.d.ts +0 -8
  113. package/dist/serpent/seal.js +0 -72
  114. package/dist/serpent/stream-pool.d.ts +0 -48
  115. package/dist/serpent/stream-pool.js +0 -275
  116. package/dist/serpent/stream-sealer.d.ts +0 -55
  117. package/dist/serpent/stream-sealer.js +0 -342
  118. package/dist/serpent/stream.d.ts +0 -28
  119. package/dist/serpent/stream.js +0 -205
  120. package/dist/serpent/stream.worker.d.ts +0 -32
  121. package/dist/serpent/stream.worker.js +0 -117
  122. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
package/dist/loader.js CHANGED
@@ -1,13 +1,27 @@
1
1
  import { base64ToBytes as _b64 } from './utils.js';
2
+ // Each WASM module gets its own fresh Memory — never shared between instances.
3
+ function makeImports() {
4
+ return { env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) } };
5
+ }
6
+ // TS 5.9 generified Uint8Array<TArrayBuffer> with default ArrayBufferLike, which
7
+ // no longer satisfies BufferSource = ArrayBufferView<ArrayBuffer> | ArrayBuffer.
8
+ // Convert Uint8Array to a proper ArrayBuffer before calling WebAssembly APIs.
9
+ function toArrayBuffer(bytes) {
10
+ if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength)
11
+ return bytes.buffer;
12
+ const buf = new ArrayBuffer(bytes.byteLength);
13
+ new Uint8Array(buf).set(bytes);
14
+ return buf;
15
+ }
2
16
  /**
3
17
  * Decode a gzip+base64 embedded WASM string to raw bytes.
4
- * Throws if the base64 is malformed (should never happen for build artifacts).
5
- * Used by loadEmbedded() and the pool worker launchers.
18
+ * Guards against missing DecompressionStream (Node <18, non-browser runtimes).
19
+ * Exported for pool worker launchers that decode blobs before spawning threads.
6
20
  */
7
21
  export async function decodeWasm(b64) {
8
22
  if (typeof DecompressionStream === 'undefined')
9
23
  throw new Error('leviathan-crypto: DecompressionStream not available — '
10
- + 'use streaming or manual init mode in this runtime');
24
+ + 'use a URL, ArrayBuffer, or WebAssembly.Module source in this runtime');
11
25
  const compressed = _b64(b64);
12
26
  if (!compressed)
13
27
  throw new Error('leviathan-crypto: corrupt embedded WASM — base64 decode failed');
@@ -30,23 +44,53 @@ export async function decodeWasm(b64) {
30
44
  }
31
45
  return out;
32
46
  }
33
- async function instantiateFromBytes(bytes) {
34
- const result = await WebAssembly.instantiate(bytes.buffer, { env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) } });
35
- return result.instance;
36
- }
37
- export async function loadEmbedded(thunk) {
38
- const b64 = await thunk();
39
- const bytes = await decodeWasm(b64);
40
- return instantiateFromBytes(bytes);
41
- }
42
- export async function loadStreaming(_mod, baseUrl, filename) {
43
- const url = new URL(filename, baseUrl).href;
44
- const result = await WebAssembly.instantiateStreaming(fetch(url), {
45
- env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) },
46
- });
47
- return result.instance;
47
+ /**
48
+ * Compile a WASM source to a Module without instantiating.
49
+ * Used by pool infrastructure to send compiled modules to workers.
50
+ */
51
+ export async function compileWasm(source) {
52
+ if (typeof source === 'string') {
53
+ if (source.length === 0)
54
+ throw new TypeError('leviathan-crypto: invalid WasmSource — empty string');
55
+ return WebAssembly.compile(toArrayBuffer(await decodeWasm(source)));
56
+ }
57
+ if (source instanceof URL)
58
+ return WebAssembly.compileStreaming(fetch(source.href));
59
+ if (source instanceof ArrayBuffer)
60
+ return WebAssembly.compile(source);
61
+ if (source instanceof Uint8Array)
62
+ return WebAssembly.compile(toArrayBuffer(source));
63
+ if (source instanceof WebAssembly.Module)
64
+ return source;
65
+ if (typeof Response !== 'undefined' && source instanceof Response)
66
+ return WebAssembly.compileStreaming(source);
67
+ if (source != null && typeof source.then === 'function')
68
+ return WebAssembly.compileStreaming(source);
69
+ throw new TypeError(`leviathan-crypto: invalid WasmSource — got ${source === null ? 'null' : typeof source}`);
48
70
  }
49
- export async function loadManual(binary) {
50
- const bytes = binary instanceof ArrayBuffer ? new Uint8Array(binary) : binary;
51
- return instantiateFromBytes(bytes);
71
+ /**
72
+ * Load a WASM module from any accepted source type.
73
+ * The loading strategy is inferred from the argument type — no mode string.
74
+ *
75
+ * Throws `TypeError` for null, numeric, or unrecognised inputs.
76
+ */
77
+ export async function loadWasm(source) {
78
+ if (typeof source === 'string') {
79
+ if (source.length === 0)
80
+ throw new TypeError('leviathan-crypto: invalid WasmSource — empty string');
81
+ return (await WebAssembly.instantiate(toArrayBuffer(await decodeWasm(source)), makeImports())).instance;
82
+ }
83
+ if (source instanceof URL)
84
+ return (await WebAssembly.instantiateStreaming(fetch(source.href), makeImports())).instance;
85
+ if (source instanceof ArrayBuffer)
86
+ return (await WebAssembly.instantiate(source, makeImports())).instance;
87
+ if (source instanceof Uint8Array)
88
+ return (await WebAssembly.instantiate(toArrayBuffer(source), makeImports())).instance;
89
+ if (source instanceof WebAssembly.Module)
90
+ return WebAssembly.instantiate(source, makeImports());
91
+ if (typeof Response !== 'undefined' && source instanceof Response)
92
+ return (await WebAssembly.instantiateStreaming(source, makeImports())).instance;
93
+ if (source != null && typeof source.then === 'function')
94
+ return (await WebAssembly.instantiateStreaming(source, makeImports())).instance;
95
+ throw new TypeError(`leviathan-crypto: invalid WasmSource — got ${source === null ? 'null' : typeof source}`);
52
96
  }
@@ -0,0 +1,4 @@
1
+ import type { CipherSuite } from '../stream/types.js';
2
+ export declare const SerpentCipher: CipherSuite & {
3
+ keygen(): Uint8Array;
4
+ };
@@ -0,0 +1,122 @@
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/cipher-suite.ts
23
+ //
24
+ // SerpentCipher — CipherSuite implementation for the STREAM construction.
25
+ // 3-key HKDF derivation, HMAC-derived CBC IV, Serpent-CBC + HMAC-SHA-256.
26
+ // Verify-then-decrypt ordering prevents padding oracle attacks (Vaudenay 2002).
27
+ import { SerpentCbc } from './serpent-cbc.js';
28
+ import { HKDF_SHA256, HMAC_SHA256 } from '../sha2/index.js';
29
+ import { constantTimeEqual, wipe, concat, randomBytes } from '../utils.js';
30
+ import { AuthenticationError } from '../errors.js';
31
+ import { getInstance } from '../init.js';
32
+ const INFO = new TextEncoder().encode('serpent-sealstream-v2');
33
+ export const SerpentCipher = {
34
+ formatEnum: 0x02,
35
+ formatName: 'serpent',
36
+ hkdfInfo: 'serpent-sealstream-v2',
37
+ keySize: 32,
38
+ kemCtSize: 0,
39
+ tagSize: 32,
40
+ padded: true,
41
+ wasmChunkSize: 65552, // src/asm/serpent/buffers.ts CHUNK_SIZE (65536 + 16 PKCS7 max overhead)
42
+ wasmModules: ['serpent', 'sha2'],
43
+ keygen() {
44
+ return randomBytes(32);
45
+ },
46
+ deriveKeys(masterKey, nonce, _kemCt) {
47
+ const hkdf = new HKDF_SHA256();
48
+ const derived = hkdf.derive(masterKey, nonce, INFO, 96);
49
+ hkdf.dispose();
50
+ // bytes[0:32]=enc_key, bytes[32:64]=mac_key, bytes[64:96]=iv_key
51
+ return { bytes: derived };
52
+ },
53
+ sealChunk(keys, counterNonce, chunk, aad) {
54
+ const encKey = keys.bytes.subarray(0, 32);
55
+ const macKey = keys.bytes.subarray(32, 64);
56
+ const ivKey = keys.bytes.subarray(64, 96);
57
+ const aadBytes = aad ?? new Uint8Array(0);
58
+ const hmac = new HMAC_SHA256();
59
+ // Derive IV from counter nonce
60
+ const ivFull = hmac.hash(ivKey, counterNonce);
61
+ const iv = ivFull.slice(0, 16);
62
+ wipe(ivFull);
63
+ // Encrypt: Serpent-CBC with PKCS7 padding
64
+ const cbc = new SerpentCbc({ dangerUnauthenticated: true });
65
+ const ct = cbc.encrypt(encKey, iv, chunk);
66
+ cbc.dispose();
67
+ // Compute HMAC tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
68
+ const aadLenBuf = new Uint8Array(4);
69
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
70
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
71
+ const tag = hmac.hash(macKey, tagInput);
72
+ hmac.dispose();
73
+ wipe(iv);
74
+ wipe(tagInput);
75
+ // Output: ct || tag (IV is NOT included)
76
+ return concat(ct, tag);
77
+ },
78
+ openChunk(keys, counterNonce, chunk, aad) {
79
+ if (chunk.length < 32)
80
+ throw new RangeError(`chunk too short for 32-byte tag (got ${chunk.length})`);
81
+ const encKey = keys.bytes.subarray(0, 32);
82
+ const macKey = keys.bytes.subarray(32, 64);
83
+ const ivKey = keys.bytes.subarray(64, 96);
84
+ const aadBytes = aad ?? new Uint8Array(0);
85
+ const ct = chunk.subarray(0, chunk.length - 32);
86
+ const receivedTag = chunk.subarray(chunk.length - 32);
87
+ const hmac = new HMAC_SHA256();
88
+ // Derive IV from counter nonce
89
+ const ivFull = hmac.hash(ivKey, counterNonce);
90
+ const iv = ivFull.slice(0, 16);
91
+ wipe(ivFull);
92
+ // Compute expected tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
93
+ const aadLenBuf = new Uint8Array(4);
94
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
95
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
96
+ const expectedTag = hmac.hash(macKey, tagInput);
97
+ hmac.dispose();
98
+ // CRITICAL: Verify HMAC BEFORE decrypting.
99
+ // Evaluating PKCS7 padding on unauthenticated data is a padding oracle (Vaudenay 2002).
100
+ if (!constantTimeEqual(expectedTag, receivedTag)) {
101
+ wipe(iv);
102
+ wipe(tagInput);
103
+ wipe(expectedTag);
104
+ getInstance('serpent').exports.wipeBuffers();
105
+ throw new AuthenticationError('serpent');
106
+ }
107
+ wipe(tagInput);
108
+ wipe(expectedTag);
109
+ // ONLY decrypt after authentication succeeds
110
+ const cbc = new SerpentCbc({ dangerUnauthenticated: true });
111
+ const plaintext = cbc.decrypt(encKey, iv, ct);
112
+ cbc.dispose();
113
+ wipe(iv);
114
+ return plaintext;
115
+ },
116
+ wipeKeys(keys) {
117
+ wipe(keys.bytes);
118
+ },
119
+ createPoolWorker() {
120
+ return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
121
+ },
122
+ };
@@ -0,0 +1 @@
1
+ export { WASM_GZ_BASE64 as serpentWasm } from '../embedded/serpent.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/serpent/embedded.ts
23
+ //
24
+ // Exports the gzip+base64 serpent WASM blob for use as a WasmSource.
25
+ // This is the only file in the serpent subpath that references the embedded blob.
26
+ // Import via `leviathan-crypto/serpent/embedded`.
27
+ export { WASM_GZ_BASE64 as serpentWasm } from '../embedded/serpent.js';
@@ -1,5 +1,6 @@
1
- import type { Mode, InitOpts } from '../init.js';
2
- export declare function serpentInit(mode?: Mode, opts?: InitOpts): Promise<void>;
1
+ import type { WasmSource } from '../wasm-source.js';
2
+ export declare function serpentInit(source: WasmSource): Promise<void>;
3
+ export type { WasmSource };
3
4
  export declare class Serpent {
4
5
  private readonly x;
5
6
  constructor();
@@ -26,39 +27,7 @@ export declare class SerpentCtr {
26
27
  decryptChunk(chunk: Uint8Array): Uint8Array;
27
28
  dispose(): void;
28
29
  }
29
- /**
30
- * Serpent-256 in CBC mode with PKCS7 padding.
31
- *
32
- * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
33
- * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
34
- */
35
- export declare class SerpentCbc {
36
- private readonly x;
37
- constructor(opts?: {
38
- dangerUnauthenticated: true;
39
- });
40
- private get mem();
41
- /**
42
- * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
43
- *
44
- * @param key 16, 24, or 32 bytes
45
- * @param iv 16 bytes — must be random and unique per (key, message)
46
- * @param plaintext any length — PKCS7 padding applied automatically
47
- * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
48
- */
49
- encrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
50
- /**
51
- * Decrypt Serpent-256 CBC + PKCS7.
52
- * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
53
- */
54
- decrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
55
- dispose(): void;
56
- private _loadKey;
57
- private _setIv;
58
- }
59
- export { SerpentSeal } from './seal.js';
60
- export { SerpentStream, sealChunk, openChunk } from './stream.js';
61
- export { SerpentStreamPool } from './stream-pool.js';
62
- export type { StreamPoolOpts } from './stream-pool.js';
63
- export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
30
+ export { SerpentCbc } from './serpent-cbc.js';
31
+ export { AuthenticationError } from '../errors.js';
32
+ export { SerpentCipher } from './cipher-suite.js';
64
33
  export declare function _serpentReady(): boolean;
@@ -22,12 +22,10 @@
22
22
  // src/ts/serpent/index.ts
23
23
  //
24
24
  // Public API classes for the Serpent-256 WASM module.
25
- // Uses the init() module cache — call init('serpent') before constructing.
25
+ // Uses the init() module cache — call serpentInit(source) before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
- import { hasSIMD } from '../utils.js';
28
- const _embedded = () => import('../embedded/serpent.js').then(m => m.WASM_GZ_BASE64);
29
- export async function serpentInit(mode = 'embedded', opts) {
30
- return initModule('serpent', _embedded, mode, opts);
27
+ export async function serpentInit(source) {
28
+ return initModule('serpent', source);
31
29
  }
32
30
  function getExports() {
33
31
  return getInstance('serpent').exports;
@@ -82,7 +80,7 @@ export class SerpentCtr {
82
80
  x;
83
81
  constructor(opts) {
84
82
  if (!opts?.dangerUnauthenticated) {
85
- throw new Error('leviathan-crypto: SerpentCtr is unauthenticated — use SerpentSeal instead. ' +
83
+ throw new Error('leviathan-crypto: SerpentCtr is unauthenticated — use Seal with SerpentCipher instead. ' +
86
84
  'To use SerpentCtr directly, pass { dangerUnauthenticated: true }.');
87
85
  }
88
86
  this.x = getExports();
@@ -106,8 +104,7 @@ export class SerpentCtr {
106
104
  const ptOff = this.x.getChunkPtOffset();
107
105
  const ctOff = this.x.getChunkCtOffset();
108
106
  mem.set(chunk, ptOff);
109
- const fn = hasSIMD() ? this.x.encryptChunk_simd : this.x.encryptChunk;
110
- fn(chunk.length);
107
+ this.x.encryptChunk_simd(chunk.length);
111
108
  return mem.slice(ctOff, ctOff + chunk.length);
112
109
  }
113
110
  beginDecrypt(key, nonce) {
@@ -120,117 +117,11 @@ export class SerpentCtr {
120
117
  this.x.wipeBuffers();
121
118
  }
122
119
  }
123
- // ── PKCS7 helpers ────────────────────────────────────────────────────────────
124
- function pkcs7Pad(data) {
125
- const padLen = 16 - (data.length % 16); // 1..16
126
- const out = new Uint8Array(data.length + padLen);
127
- out.set(data);
128
- out.fill(padLen, data.length);
129
- return out;
130
- }
131
- function pkcs7Strip(data) {
132
- if (data.length === 0)
133
- throw new RangeError('empty ciphertext');
134
- const padLen = data[data.length - 1];
135
- if (padLen === 0 || padLen > 16)
136
- throw new RangeError(`invalid PKCS7 padding byte: ${padLen}`);
137
- if (padLen > data.length)
138
- throw new RangeError(`invalid PKCS7 padding: pad length ${padLen} exceeds data length ${data.length}`);
139
- let bad = 0;
140
- for (let i = data.length - padLen; i < data.length; i++)
141
- bad |= data[i] ^ padLen;
142
- if (bad !== 0)
143
- throw new RangeError('invalid PKCS7 padding');
144
- return data.subarray(0, data.length - padLen);
145
- }
146
120
  // ── SerpentCbc ───────────────────────────────────────────────────────────────
147
- /**
148
- * Serpent-256 in CBC mode with PKCS7 padding.
149
- *
150
- * **WARNING: CBC mode is unauthenticated.** Always authenticate the output
151
- * with HMAC-SHA256 (Encrypt-then-MAC) or use `XChaCha20Poly1305` instead.
152
- */
153
- export class SerpentCbc {
154
- x;
155
- constructor(opts) {
156
- if (!opts?.dangerUnauthenticated) {
157
- throw new Error('leviathan-crypto: SerpentCbc is unauthenticated — use SerpentSeal instead. ' +
158
- 'To use SerpentCbc directly, pass { dangerUnauthenticated: true }.');
159
- }
160
- this.x = getExports();
161
- }
162
- get mem() {
163
- return new Uint8Array(this.x.memory.buffer);
164
- }
165
- /**
166
- * Encrypt plaintext with Serpent-256 CBC + PKCS7 padding.
167
- *
168
- * @param key 16, 24, or 32 bytes
169
- * @param iv 16 bytes — must be random and unique per (key, message)
170
- * @param plaintext any length — PKCS7 padding applied automatically
171
- * @returns ciphertext (length = ceil((plaintext.length + 1) / 16) * 16)
172
- */
173
- encrypt(key, iv, plaintext) {
174
- this._loadKey(key);
175
- this._setIv(iv);
176
- const padded = pkcs7Pad(plaintext);
177
- const output = new Uint8Array(padded.length);
178
- const ptOff = this.x.getChunkPtOffset();
179
- const ctOff = this.x.getChunkCtOffset();
180
- const maxChunk = 65536;
181
- for (let off = 0; off < padded.length; off += maxChunk) {
182
- const chunk = padded.subarray(off, Math.min(off + maxChunk, padded.length));
183
- this.mem.set(chunk, ptOff);
184
- this.x.cbcEncryptChunk(chunk.length);
185
- output.set(new Uint8Array(this.x.memory.buffer).subarray(ctOff, ctOff + chunk.length), off);
186
- }
187
- return output;
188
- }
189
- /**
190
- * Decrypt Serpent-256 CBC + PKCS7.
191
- * Throws if ciphertext length is not a non-zero multiple of 16 or PKCS7 is invalid.
192
- */
193
- decrypt(key, iv, ciphertext) {
194
- if (ciphertext.length === 0 || ciphertext.length % 16 !== 0)
195
- throw new RangeError('ciphertext length must be a non-zero multiple of 16');
196
- this._loadKey(key);
197
- this._setIv(iv);
198
- const output = new Uint8Array(ciphertext.length);
199
- const ctOff = this.x.getChunkCtOffset();
200
- const ptOff = this.x.getChunkPtOffset();
201
- const maxChunk = 65536;
202
- for (let off = 0; off < ciphertext.length; off += maxChunk) {
203
- const chunk = ciphertext.subarray(off, Math.min(off + maxChunk, ciphertext.length));
204
- this.mem.set(chunk, ctOff);
205
- const fn = hasSIMD() ? this.x.cbcDecryptChunk_simd : this.x.cbcDecryptChunk;
206
- fn(chunk.length);
207
- output.set(new Uint8Array(this.x.memory.buffer).subarray(ptOff, ptOff + chunk.length), off);
208
- }
209
- return pkcs7Strip(output);
210
- }
211
- dispose() {
212
- this.x.wipeBuffers();
213
- }
214
- _loadKey(key) {
215
- if (key.length !== 16 && key.length !== 24 && key.length !== 32)
216
- throw new RangeError(`Serpent key must be 16, 24, or 32 bytes (got ${key.length})`);
217
- this.mem.set(key, this.x.getKeyOffset());
218
- this.x.loadKey(key.length);
219
- }
220
- _setIv(iv) {
221
- if (iv.length !== 16)
222
- throw new RangeError(`CBC IV must be 16 bytes (got ${iv.length})`);
223
- this.mem.set(iv, this.x.getCbcIvOffset());
224
- }
225
- }
226
- // ── SerpentSeal re-export ─────────────────────────────────────────────────────
227
- export { SerpentSeal } from './seal.js';
228
- // ── SerpentStream re-export ───────────────────────────────────────────────────
229
- export { SerpentStream, sealChunk, openChunk } from './stream.js';
230
- // ── SerpentStreamPool re-export ───────────────────────────────────────────────
231
- export { SerpentStreamPool } from './stream-pool.js';
232
- // ── SerpentStreamSealer / SerpentStreamOpener re-export ───────────────────────
233
- export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
121
+ export { SerpentCbc } from './serpent-cbc.js';
122
+ export { AuthenticationError } from '../errors.js';
123
+ // ── SerpentCipher re-export ───────────────────────────────────────────────────
124
+ export { SerpentCipher } from './cipher-suite.js';
234
125
  // ── Ready check ──────────────────────────────────────────────────────────────
235
126
  export function _serpentReady() {
236
127
  try {
@@ -0,0 +1 @@
1
+ export {};