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
package/dist/loader.js CHANGED
@@ -1,30 +1,96 @@
1
- function base64ToBytes(b64) {
2
- if (typeof atob === 'function') {
3
- const raw = atob(b64);
4
- const out = new Uint8Array(raw.length);
5
- for (let i = 0; i < raw.length; i++)
6
- out[i] = raw.charCodeAt(i);
7
- return out;
8
- }
9
- return new Uint8Array(Buffer.from(b64, 'base64'));
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 }) } };
10
5
  }
11
- async function instantiateFromBytes(bytes) {
12
- const result = await WebAssembly.instantiate(bytes.buffer, { env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) } });
13
- return result.instance;
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;
14
15
  }
15
- export async function loadEmbedded(thunk) {
16
- const b64 = await thunk();
17
- const bytes = base64ToBytes(b64);
18
- return instantiateFromBytes(bytes);
16
+ /**
17
+ * Decode a gzip+base64 embedded WASM string to raw bytes.
18
+ * Guards against missing DecompressionStream (Node <18, non-browser runtimes).
19
+ * Exported for pool worker launchers that decode blobs before spawning threads.
20
+ */
21
+ export async function decodeWasm(b64) {
22
+ if (typeof DecompressionStream === 'undefined')
23
+ throw new Error('leviathan-crypto: DecompressionStream not available — '
24
+ + 'use a URL, ArrayBuffer, or WebAssembly.Module source in this runtime');
25
+ const compressed = _b64(b64);
26
+ if (!compressed)
27
+ throw new Error('leviathan-crypto: corrupt embedded WASM — base64 decode failed');
28
+ const ds = new DecompressionStream('gzip');
29
+ const writer = ds.writable.getWriter();
30
+ const reader = ds.readable.getReader();
31
+ const writePromise = writer.write(compressed).then(() => writer.close());
32
+ const chunks = [];
33
+ let done, value;
34
+ while ({ done, value } = await reader.read(), !done)
35
+ if (value)
36
+ chunks.push(value);
37
+ await writePromise;
38
+ const len = chunks.reduce((s, c) => s + c.length, 0);
39
+ const out = new Uint8Array(len);
40
+ let off = 0;
41
+ for (const c of chunks) {
42
+ out.set(c, off);
43
+ off += c.length;
44
+ }
45
+ return out;
19
46
  }
20
- export async function loadStreaming(_mod, baseUrl, filename) {
21
- const url = new URL(filename, baseUrl).href;
22
- const result = await WebAssembly.instantiateStreaming(fetch(url), {
23
- env: { memory: new WebAssembly.Memory({ initial: 3, maximum: 3 }) },
24
- });
25
- 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}`);
26
70
  }
27
- export async function loadManual(binary) {
28
- const bytes = binary instanceof ArrayBuffer ? new Uint8Array(binary) : binary;
29
- 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}`);
30
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,121 @@
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
+ wasmModules: ['serpent', 'sha2'],
42
+ keygen() {
43
+ return randomBytes(32);
44
+ },
45
+ deriveKeys(masterKey, nonce, _kemCt) {
46
+ const hkdf = new HKDF_SHA256();
47
+ const derived = hkdf.derive(masterKey, nonce, INFO, 96);
48
+ hkdf.dispose();
49
+ // bytes[0:32]=enc_key, bytes[32:64]=mac_key, bytes[64:96]=iv_key
50
+ return { bytes: derived };
51
+ },
52
+ sealChunk(keys, counterNonce, chunk, aad) {
53
+ const encKey = keys.bytes.subarray(0, 32);
54
+ const macKey = keys.bytes.subarray(32, 64);
55
+ const ivKey = keys.bytes.subarray(64, 96);
56
+ const aadBytes = aad ?? new Uint8Array(0);
57
+ const hmac = new HMAC_SHA256();
58
+ // Derive IV from counter nonce
59
+ const ivFull = hmac.hash(ivKey, counterNonce);
60
+ const iv = ivFull.slice(0, 16);
61
+ wipe(ivFull);
62
+ // Encrypt: Serpent-CBC with PKCS7 padding
63
+ const cbc = new SerpentCbc({ dangerUnauthenticated: true });
64
+ const ct = cbc.encrypt(encKey, iv, chunk);
65
+ cbc.dispose();
66
+ // Compute HMAC tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
67
+ const aadLenBuf = new Uint8Array(4);
68
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
69
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
70
+ const tag = hmac.hash(macKey, tagInput);
71
+ hmac.dispose();
72
+ wipe(iv);
73
+ wipe(tagInput);
74
+ // Output: ct || tag (IV is NOT included)
75
+ return concat(ct, tag);
76
+ },
77
+ openChunk(keys, counterNonce, chunk, aad) {
78
+ if (chunk.length < 32)
79
+ throw new RangeError(`chunk too short for 32-byte tag (got ${chunk.length})`);
80
+ const encKey = keys.bytes.subarray(0, 32);
81
+ const macKey = keys.bytes.subarray(32, 64);
82
+ const ivKey = keys.bytes.subarray(64, 96);
83
+ const aadBytes = aad ?? new Uint8Array(0);
84
+ const ct = chunk.subarray(0, chunk.length - 32);
85
+ const receivedTag = chunk.subarray(chunk.length - 32);
86
+ const hmac = new HMAC_SHA256();
87
+ // Derive IV from counter nonce
88
+ const ivFull = hmac.hash(ivKey, counterNonce);
89
+ const iv = ivFull.slice(0, 16);
90
+ wipe(ivFull);
91
+ // Compute expected tag: HMAC-SHA-256(mac_key, counterNonce || u32be(aad_len) || aad || ct)
92
+ const aadLenBuf = new Uint8Array(4);
93
+ new DataView(aadLenBuf.buffer).setUint32(0, aadBytes.length, false);
94
+ const tagInput = concat(counterNonce, aadLenBuf, aadBytes, ct);
95
+ const expectedTag = hmac.hash(macKey, tagInput);
96
+ hmac.dispose();
97
+ // CRITICAL: Verify HMAC BEFORE decrypting.
98
+ // Evaluating PKCS7 padding on unauthenticated data is a padding oracle (Vaudenay 2002).
99
+ if (!constantTimeEqual(expectedTag, receivedTag)) {
100
+ wipe(iv);
101
+ wipe(tagInput);
102
+ wipe(expectedTag);
103
+ getInstance('serpent').exports.wipeBuffers();
104
+ throw new AuthenticationError('serpent');
105
+ }
106
+ wipe(tagInput);
107
+ wipe(expectedTag);
108
+ // ONLY decrypt after authentication succeeds
109
+ const cbc = new SerpentCbc({ dangerUnauthenticated: true });
110
+ const plaintext = cbc.decrypt(encKey, iv, ct);
111
+ cbc.dispose();
112
+ wipe(iv);
113
+ return plaintext;
114
+ },
115
+ wipeKeys(keys) {
116
+ wipe(keys.bytes);
117
+ },
118
+ createPoolWorker() {
119
+ return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
120
+ },
121
+ };
@@ -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_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 {};