leviathan-crypto 1.4.0 → 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 (119) hide show
  1. package/CLAUDE.md +129 -94
  2. package/README.md +166 -223
  3. package/SECURITY.md +85 -45
  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 -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 +320 -0
  17. package/dist/docs/architecture.md +419 -285
  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 +93 -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/errors.d.ts +10 -0
  32. package/dist/errors.js +38 -0
  33. package/dist/fortuna.d.ts +0 -6
  34. package/dist/fortuna.js +5 -5
  35. package/dist/index.d.ts +25 -9
  36. package/dist/index.js +36 -7
  37. package/dist/init.d.ts +3 -7
  38. package/dist/init.js +18 -35
  39. package/dist/keccak/embedded.d.ts +1 -0
  40. package/dist/keccak/embedded.js +27 -0
  41. package/dist/keccak/index.d.ts +4 -0
  42. package/dist/keccak/index.js +31 -0
  43. package/dist/kyber/embedded.d.ts +1 -0
  44. package/dist/kyber/embedded.js +27 -0
  45. package/dist/kyber/indcpa.d.ts +49 -0
  46. package/dist/kyber/indcpa.js +352 -0
  47. package/dist/kyber/index.d.ts +38 -0
  48. package/dist/kyber/index.js +150 -0
  49. package/dist/kyber/kem.d.ts +21 -0
  50. package/dist/kyber/kem.js +160 -0
  51. package/dist/kyber/params.d.ts +14 -0
  52. package/dist/kyber/params.js +37 -0
  53. package/dist/kyber/suite.d.ts +13 -0
  54. package/dist/kyber/suite.js +93 -0
  55. package/dist/kyber/types.d.ts +98 -0
  56. package/dist/kyber/types.js +25 -0
  57. package/dist/kyber/validate.d.ts +19 -0
  58. package/dist/kyber/validate.js +68 -0
  59. package/dist/kyber.wasm +0 -0
  60. package/dist/loader.d.ts +15 -6
  61. package/dist/loader.js +65 -21
  62. package/dist/serpent/cipher-suite.d.ts +4 -0
  63. package/dist/serpent/cipher-suite.js +121 -0
  64. package/dist/serpent/embedded.d.ts +1 -0
  65. package/dist/serpent/embedded.js +27 -0
  66. package/dist/serpent/index.d.ts +6 -37
  67. package/dist/serpent/index.js +9 -118
  68. package/dist/serpent/pool-worker.d.ts +1 -0
  69. package/dist/serpent/pool-worker.js +202 -0
  70. package/dist/serpent/serpent-cbc.d.ts +30 -0
  71. package/dist/serpent/serpent-cbc.js +136 -0
  72. package/dist/sha2/embedded.d.ts +1 -0
  73. package/dist/sha2/embedded.js +27 -0
  74. package/dist/sha2/hkdf.js +6 -2
  75. package/dist/sha2/index.d.ts +3 -2
  76. package/dist/sha2/index.js +3 -4
  77. package/dist/sha3/embedded.d.ts +1 -0
  78. package/dist/sha3/embedded.js +27 -0
  79. package/dist/sha3/index.d.ts +3 -2
  80. package/dist/sha3/index.js +3 -4
  81. package/dist/stream/constants.d.ts +6 -0
  82. package/dist/stream/constants.js +30 -0
  83. package/dist/stream/header.d.ts +9 -0
  84. package/dist/stream/header.js +77 -0
  85. package/dist/stream/index.d.ts +7 -0
  86. package/dist/stream/index.js +27 -0
  87. package/dist/stream/open-stream.d.ts +21 -0
  88. package/dist/stream/open-stream.js +146 -0
  89. package/dist/stream/seal-stream-pool.d.ts +38 -0
  90. package/dist/stream/seal-stream-pool.js +391 -0
  91. package/dist/stream/seal-stream.d.ts +20 -0
  92. package/dist/stream/seal-stream.js +142 -0
  93. package/dist/stream/seal.d.ts +9 -0
  94. package/dist/stream/seal.js +75 -0
  95. package/dist/stream/types.d.ts +24 -0
  96. package/dist/stream/types.js +26 -0
  97. package/dist/utils.d.ts +7 -2
  98. package/dist/utils.js +49 -3
  99. package/dist/wasm-source.d.ts +12 -0
  100. package/dist/wasm-source.js +26 -0
  101. package/package.json +13 -5
  102. package/dist/chacha20/pool.d.ts +0 -52
  103. package/dist/chacha20/pool.js +0 -178
  104. package/dist/chacha20/pool.worker.js +0 -37
  105. package/dist/chacha20/stream-sealer.d.ts +0 -49
  106. package/dist/chacha20/stream-sealer.js +0 -327
  107. package/dist/docs/chacha20_pool.md +0 -309
  108. package/dist/docs/wasm.md +0 -194
  109. package/dist/serpent/seal.d.ts +0 -8
  110. package/dist/serpent/seal.js +0 -72
  111. package/dist/serpent/stream-pool.d.ts +0 -48
  112. package/dist/serpent/stream-pool.js +0 -275
  113. package/dist/serpent/stream-sealer.d.ts +0 -55
  114. package/dist/serpent/stream-sealer.js +0 -342
  115. package/dist/serpent/stream.d.ts +0 -28
  116. package/dist/serpent/stream.js +0 -205
  117. package/dist/serpent/stream.worker.d.ts +0 -32
  118. package/dist/serpent/stream.worker.js +0 -117
  119. /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
@@ -0,0 +1,142 @@
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/seal-stream.ts
23
+ //
24
+ // SealStream — cipher-agnostic streaming encryption using the STREAM
25
+ // construction (Hoang/Reyhanitabar/Rogaway/Vizár, CRYPTO 2015).
26
+ import { randomBytes, concat } from '../utils.js';
27
+ import { isInitialized } from '../init.js';
28
+ import { CHUNK_MIN, CHUNK_MAX, TAG_DATA, TAG_FINAL } from './constants.js';
29
+ import { writeHeader, makeCounterNonce } from './header.js';
30
+ function u32beFrame(n) {
31
+ const b = new Uint8Array(4);
32
+ new DataView(b.buffer).setUint32(0, n, false);
33
+ return b;
34
+ }
35
+ // Module-level nonce injection slot — used only by _fromNonce for KAT tests.
36
+ // Set immediately before constructing, cleared inside the constructor.
37
+ let _injectNonce;
38
+ export class SealStream {
39
+ /** Preamble sent before the first chunk: header [|| kemCiphertext]. */
40
+ preamble;
41
+ cipher;
42
+ keys;
43
+ chunkSize;
44
+ framed;
45
+ counter = 0;
46
+ state = 'ready';
47
+ constructor(cipher, key, opts) {
48
+ this.cipher = cipher;
49
+ this.chunkSize = opts?.chunkSize ?? 65536;
50
+ this.framed = opts?.framed ?? false;
51
+ if (!isInitialized('sha2'))
52
+ throw new Error('leviathan-crypto: stream layer requires sha2 for key derivation — '
53
+ + 'call init({ sha2: ... }) before creating a SealStream');
54
+ if (key.length !== cipher.keySize)
55
+ throw new RangeError(`key must be ${cipher.keySize} bytes (got ${key.length})`);
56
+ if (this.chunkSize < CHUNK_MIN || this.chunkSize > CHUNK_MAX)
57
+ throw new RangeError(`chunkSize must be in [${CHUNK_MIN}, ${CHUNK_MAX}] (got ${this.chunkSize})`);
58
+ const nonce = _injectNonce ?? randomBytes(16);
59
+ _injectNonce = undefined;
60
+ this.keys = cipher.deriveKeys(key, nonce);
61
+ const kemCt = this.keys.kemCiphertext;
62
+ const header = writeHeader(cipher.formatEnum, this.framed, nonce, this.chunkSize);
63
+ this.preamble = kemCt ? concat(header, kemCt) : header;
64
+ }
65
+ /**
66
+ * @internal
67
+ * KAT-only factory — injects a fixed nonce so seal output is deterministic.
68
+ * Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
69
+ */
70
+ static _fromNonce(cipher, key, opts, nonce) {
71
+ if (nonce.length !== 16)
72
+ throw new RangeError(`_nonce must be 16 bytes (got ${nonce.length})`);
73
+ _injectNonce = nonce;
74
+ try {
75
+ return new SealStream(cipher, key, opts);
76
+ }
77
+ finally {
78
+ _injectNonce = undefined;
79
+ }
80
+ }
81
+ push(chunk, opts) {
82
+ if (this.state !== 'ready')
83
+ throw new Error('SealStream: cannot push after finalize');
84
+ if (chunk.length > this.chunkSize)
85
+ throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
86
+ const nonce = makeCounterNonce(this.counter, TAG_DATA);
87
+ const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
88
+ this.counter++;
89
+ return this.framed ? concat(u32beFrame(result.length), result) : result;
90
+ }
91
+ finalize(chunk, opts) {
92
+ if (this.state !== 'ready')
93
+ throw new Error('SealStream: already finalized');
94
+ if (chunk.length > this.chunkSize)
95
+ throw new RangeError(`chunk exceeds chunkSize (${chunk.length} > ${this.chunkSize})`);
96
+ const nonce = makeCounterNonce(this.counter, TAG_FINAL);
97
+ const result = this.cipher.sealChunk(this.keys, nonce, chunk, opts?.aad);
98
+ this.cipher.wipeKeys(this.keys);
99
+ this.state = 'finalized';
100
+ return this.framed ? concat(u32beFrame(result.length), result) : result;
101
+ }
102
+ dispose() {
103
+ if (this.state === 'ready') {
104
+ this.cipher.wipeKeys(this.keys);
105
+ this.state = 'finalized';
106
+ }
107
+ }
108
+ toTransformStream() {
109
+ let headerSent = false;
110
+ let buffered = null;
111
+ return new TransformStream({
112
+ transform: (chunk, controller) => {
113
+ try {
114
+ if (!headerSent) {
115
+ controller.enqueue(this.preamble);
116
+ headerSent = true;
117
+ }
118
+ if (buffered !== null) {
119
+ controller.enqueue(this.push(buffered));
120
+ }
121
+ buffered = chunk;
122
+ }
123
+ catch (err) {
124
+ this.dispose();
125
+ throw err;
126
+ }
127
+ },
128
+ flush: (controller) => {
129
+ try {
130
+ if (!headerSent) {
131
+ controller.enqueue(this.preamble);
132
+ }
133
+ controller.enqueue(this.finalize(buffered ?? new Uint8Array(0)));
134
+ }
135
+ catch (err) {
136
+ this.dispose();
137
+ throw err;
138
+ }
139
+ },
140
+ });
141
+ }
142
+ }
@@ -0,0 +1,9 @@
1
+ import type { CipherSuite } from './types.js';
2
+ export declare class Seal {
3
+ static encrypt(suite: CipherSuite, key: Uint8Array, pt: Uint8Array, opts?: {
4
+ aad?: Uint8Array;
5
+ }): Uint8Array;
6
+ static decrypt(suite: CipherSuite, key: Uint8Array, blob: Uint8Array, opts?: {
7
+ aad?: Uint8Array;
8
+ }): Uint8Array;
9
+ }
@@ -0,0 +1,75 @@
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/seal.ts
23
+ //
24
+ // Seal — unified single-shot encrypt/decrypt using the STREAM construction.
25
+ // Seal blobs are valid SealStream blobs with a single final chunk.
26
+ // OpenStream can decrypt a Seal blob without modification.
27
+ import { concat } from '../utils.js';
28
+ import { SealStream } from './seal-stream.js';
29
+ import { OpenStream } from './open-stream.js';
30
+ import { HEADER_SIZE, CHUNK_MAX, CHUNK_MIN } from './constants.js';
31
+ // eslint-disable-next-line @typescript-eslint/no-extraneous-class -- static-only class required for stripInternal to strip _fromNonce from .d.ts
32
+ export class Seal {
33
+ static encrypt(suite, key, pt, opts) {
34
+ if (pt.length > CHUNK_MAX)
35
+ throw new RangeError(`Seal.encrypt: plaintext exceeds maximum (${CHUNK_MAX} bytes) — use SealStream for large data`);
36
+ const sealer = new SealStream(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) });
37
+ try {
38
+ const ct = sealer.finalize(pt, opts);
39
+ return concat(sealer.preamble, ct);
40
+ }
41
+ finally {
42
+ sealer.dispose();
43
+ }
44
+ }
45
+ static decrypt(suite, key, blob, opts) {
46
+ const preambleLen = HEADER_SIZE + suite.kemCtSize;
47
+ if (blob.length < preambleLen)
48
+ throw new RangeError(`Seal.decrypt: blob too short — need at least ${preambleLen} bytes (got ${blob.length})`);
49
+ const preamble = blob.subarray(0, preambleLen);
50
+ const opener = new OpenStream(suite, key, preamble);
51
+ try {
52
+ return opener.finalize(blob.subarray(preambleLen), opts);
53
+ }
54
+ finally {
55
+ opener.dispose();
56
+ }
57
+ }
58
+ /**
59
+ * @internal
60
+ * KAT-only — injects a fixed nonce so output is deterministic.
61
+ * Stripped from published `.d.ts` by `stripInternal`. Do not use in production.
62
+ */
63
+ static _fromNonce(suite, key, pt, nonce, opts) {
64
+ if (pt.length > CHUNK_MAX)
65
+ throw new RangeError(`Seal._fromNonce: plaintext exceeds maximum (${CHUNK_MAX} bytes) — use SealStream for large data`);
66
+ const sealer = SealStream._fromNonce(suite, key, { chunkSize: Math.max(pt.length, CHUNK_MIN) }, nonce);
67
+ try {
68
+ const ct = sealer.finalize(pt, opts);
69
+ return concat(sealer.preamble, ct);
70
+ }
71
+ finally {
72
+ sealer.dispose();
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,24 @@
1
+ export interface DerivedKeys {
2
+ readonly bytes: Uint8Array;
3
+ readonly kemCiphertext?: Uint8Array;
4
+ }
5
+ export interface CipherSuite {
6
+ readonly formatEnum: number;
7
+ readonly formatName: string;
8
+ readonly hkdfInfo: string;
9
+ readonly keySize: number;
10
+ readonly decKeySize?: number;
11
+ readonly kemCtSize: number;
12
+ readonly tagSize: number;
13
+ readonly padded: boolean;
14
+ deriveKeys(key: Uint8Array, nonce: Uint8Array, kemCt?: Uint8Array): DerivedKeys;
15
+ sealChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
16
+ openChunk(keys: DerivedKeys, counterNonce: Uint8Array, chunk: Uint8Array, aad?: Uint8Array): Uint8Array;
17
+ wipeKeys(keys: DerivedKeys): void;
18
+ readonly wasmModules: readonly string[];
19
+ createPoolWorker(): Worker;
20
+ }
21
+ export interface SealStreamOpts {
22
+ chunkSize?: number;
23
+ framed?: boolean;
24
+ }
@@ -0,0 +1,26 @@
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/types.ts
23
+ //
24
+ // CipherSuite interface — cipher-specific logic injected into SealStream
25
+ // and OpenStream. Implementations are plain objects (not classes).
26
+ export {};
package/dist/utils.d.ts CHANGED
@@ -10,10 +10,14 @@ export declare const bytesToUtf8: (bytes: Uint8Array) => string;
10
10
  export declare const base64ToBytes: (b64: string) => Uint8Array | undefined;
11
11
  /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 — no padding characters). */
12
12
  export declare const bytesToBase64: (bytes: Uint8Array, url?: boolean) => string;
13
+ export declare const CT_MAX_BYTES = 32768;
13
14
  /**
14
15
  * Constant-time byte-array equality.
15
- * XOR-accumulate pattern no early return on mismatch.
16
+ * Uses WASM SIMD when available (no JIT short-circuiting, no speculative
17
+ * optimization). Falls back to a JS XOR-accumulate loop on runtimes
18
+ * without SIMD support.
16
19
  * Length check is not constant-time (length is non-secret in all protocols).
20
+ * Max input size: 32768 bytes per side (enforced regardless of code path).
17
21
  */
18
22
  export declare const constantTimeEqual: (a: Uint8Array, b: Uint8Array) => boolean;
19
23
  /** Zero a typed array in place. */
@@ -26,6 +30,7 @@ export declare const concat: (...arrays: Uint8Array[]) => Uint8Array;
26
30
  export declare const randomBytes: (n: number) => Uint8Array;
27
31
  /**
28
32
  * Detects WASM SIMD support once and caches the result.
29
- * Gates CTR/CBC-decrypt dispatch in Serpent and encryptChunk dispatch in ChaCha20.
33
+ * Used by init() to preflight-check before loading serpent/chacha20 modules.
34
+ * Exported for consumers who want to feature-detect before calling init().
30
35
  */
31
36
  export declare function hasSIMD(): boolean;
package/dist/utils.js CHANGED
@@ -136,15 +136,60 @@ export const bytesToBase64 = (bytes, url = false) => {
136
136
  }
137
137
  return base64;
138
138
  };
139
- // ── Crypto utilities ─────────────────────────────────────────────────────────
139
+ // ── Constant-time comparison ─────────────────────────────────────────────────
140
+ import { CT_WASM } from './ct-wasm.js';
141
+ let _ctCompare = null;
142
+ let _ctMem = null;
143
+ let _ctInit = false;
144
+ // CT WASM module uses 1 page (64KB) of linear memory with both buffers
145
+ // laid out side-by-side: a at offset 0, b at offset a.length.
146
+ // Max per-side = _ctMem.buffer.byteLength >>> 1 = 32768 bytes.
147
+ // In practice the largest comparison is a 32-byte HMAC-SHA-256 tag.
148
+ export const CT_MAX_BYTES = 32768;
149
+ /** Try to compile the SIMD WASM ct module. Returns false if unavailable. */
150
+ function _initCt() {
151
+ if (_ctInit)
152
+ return _ctCompare !== null;
153
+ _ctInit = true;
154
+ try {
155
+ if (!hasSIMD())
156
+ return false;
157
+ _ctMem = new WebAssembly.Memory({ initial: 1, maximum: 1 });
158
+ const buf = CT_WASM.buffer.slice(CT_WASM.byteOffset, CT_WASM.byteOffset + CT_WASM.byteLength);
159
+ const mod = new WebAssembly.Module(buf);
160
+ const inst = new WebAssembly.Instance(mod, { env: { memory: _ctMem } });
161
+ _ctCompare = inst.exports.compare;
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
140
168
  /**
141
169
  * Constant-time byte-array equality.
142
- * XOR-accumulate pattern no early return on mismatch.
170
+ * Uses WASM SIMD when available (no JIT short-circuiting, no speculative
171
+ * optimization). Falls back to a JS XOR-accumulate loop on runtimes
172
+ * without SIMD support.
143
173
  * Length check is not constant-time (length is non-secret in all protocols).
174
+ * Max input size: 32768 bytes per side (enforced regardless of code path).
144
175
  */
145
176
  export const constantTimeEqual = (a, b) => {
146
177
  if (a.length !== b.length)
147
178
  return false;
179
+ if (a.length > CT_MAX_BYTES)
180
+ throw new RangeError(`constantTimeEqual: max ${CT_MAX_BYTES} bytes (got ${a.length})`);
181
+ if (_initCt() && _ctMem && _ctCompare) {
182
+ const mem = new Uint8Array(_ctMem.buffer);
183
+ mem.set(a, 0);
184
+ mem.set(b, a.length);
185
+ try {
186
+ return _ctCompare(0, a.length, a.length) === 1;
187
+ }
188
+ finally {
189
+ mem.fill(0, 0, a.length * 2);
190
+ }
191
+ }
192
+ // JS fallback — best-effort constant-time via XOR accumulate
148
193
  let diff = 0;
149
194
  for (let i = 0; i < a.length; i++)
150
195
  diff |= a[i] ^ b[i];
@@ -181,7 +226,8 @@ export const randomBytes = (n) => {
181
226
  let _simd = null;
182
227
  /**
183
228
  * Detects WASM SIMD support once and caches the result.
184
- * Gates CTR/CBC-decrypt dispatch in Serpent and encryptChunk dispatch in ChaCha20.
229
+ * Used by init() to preflight-check before loading serpent/chacha20 modules.
230
+ * Exported for consumers who want to feature-detect before calling init().
185
231
  */
186
232
  export function hasSIMD() {
187
233
  if (_simd !== null)
@@ -0,0 +1,12 @@
1
+ /**
2
+ * All accepted forms of WASM input for init functions.
3
+ *
4
+ * - `string` — gzip+base64 embedded blob (from `/embedded` subpath)
5
+ * - `URL` — fetched via `WebAssembly.instantiateStreaming`
6
+ * - `ArrayBuffer` — raw WASM bytes, compiled inline
7
+ * - `Uint8Array` — raw WASM bytes, compiled inline
8
+ * - `WebAssembly.Module` — pre-compiled module (Cloudflare Workers, edge runtimes)
9
+ * - `Response` — streaming instantiation from an in-flight fetch response
10
+ * - `Promise<Response>` — streaming instantiation from a deferred fetch
11
+ */
12
+ export type WasmSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module | Response | Promise<Response>;
@@ -0,0 +1,26 @@
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/wasm-source.ts
23
+ //
24
+ // Union type for all accepted WASM loading strategies.
25
+ // The argument type determines the loading path — no mode string required.
26
+ export {};
package/package.json CHANGED
@@ -1,18 +1,26 @@
1
1
  {
2
2
  "name": "leviathan-crypto",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "author": "xero (https://x-e.ro)",
5
5
  "license": "MIT",
6
- "description": "Zero-dependency WebAssembly cryptography library for TypeScript: Serpent-256, XChaCha20-Poly1305, SHA-2/3, HMAC, HKDF, and Fortuna CSPRNG, with a strictly typed API built on vector-verified primitives.",
6
+ "description": "Zero-dependency WASM cryptography for TypeScript. Paranoid ciphers, post-quantum key encapsulation, and Fortuna CSPRNG behind a strictly typed API. All computation runs outside the JS JIT on vector-verified primitives.",
7
7
  "type": "module",
8
8
  "sideEffects": false,
9
9
  "exports": {
10
10
  ".": "./dist/index.js",
11
+ "./stream": "./dist/stream/index.js",
11
12
  "./serpent": "./dist/serpent/index.js",
13
+ "./serpent/embedded": "./dist/serpent/embedded.js",
12
14
  "./chacha20": "./dist/chacha20/index.js",
13
- "./chacha20/pool": "./dist/chacha20/pool.js",
15
+ "./chacha20/embedded": "./dist/chacha20/embedded.js",
14
16
  "./sha2": "./dist/sha2/index.js",
15
- "./sha3": "./dist/sha3/index.js"
17
+ "./sha2/embedded": "./dist/sha2/embedded.js",
18
+ "./sha3": "./dist/sha3/index.js",
19
+ "./sha3/embedded": "./dist/sha3/embedded.js",
20
+ "./keccak": "./dist/keccak/index.js",
21
+ "./keccak/embedded": "./dist/keccak/embedded.js",
22
+ "./kyber": "./dist/kyber/index.js",
23
+ "./kyber/embedded": "./dist/kyber/embedded.js"
16
24
  },
17
25
  "types": "./dist/index.d.ts",
18
26
  "files": [
@@ -24,7 +32,7 @@
24
32
  "type": "git",
25
33
  "url": "git+https://github.com/xero/leviathan-crypto.git"
26
34
  },
27
- "homepage": "https://github.com/xero/leviathan-crypto/wiki",
35
+ "homepage": "https://leviathan.3xi.club",
28
36
  "bugs": {
29
37
  "url": "https://github.com/xero/leviathan-crypto/issues"
30
38
  },
@@ -1,52 +0,0 @@
1
- export interface PoolOpts {
2
- /** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
3
- workers?: number;
4
- }
5
- /**
6
- * Parallel worker pool for XChaCha20-Poly1305 AEAD.
7
- *
8
- * Each worker owns its own `WebAssembly.Instance` with isolated linear memory.
9
- * Jobs are dispatched round-robin to idle workers; excess jobs queue until a
10
- * worker frees up.
11
- *
12
- * **Warning:** Input buffers (`key`, `nonce`, `plaintext`/`ciphertext`, `aad`)
13
- * are transferred to the worker and neutered on the calling side. The caller
14
- * must copy any buffer they need to retain after calling `encrypt()`/`decrypt()`.
15
- */
16
- export declare class XChaCha20Poly1305Pool {
17
- private readonly _workers;
18
- private readonly _idle;
19
- private readonly _queue;
20
- private readonly _pending;
21
- private _nextId;
22
- private _disposed;
23
- private constructor();
24
- /**
25
- * Create a new pool. Requires `init(['chacha20'])` to have been called.
26
- * Compiles the WASM module once and distributes it to all workers.
27
- */
28
- static create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>;
29
- /**
30
- * Encrypt plaintext with XChaCha20-Poly1305.
31
- * Returns `ciphertext || tag` (plaintext.length + 16 bytes).
32
- *
33
- * **Warning:** All input buffers are transferred and neutered after dispatch.
34
- */
35
- encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
36
- /**
37
- * Decrypt ciphertext with XChaCha20-Poly1305.
38
- * Input is `ciphertext || tag` (at least 16 bytes).
39
- *
40
- * **Warning:** All input buffers are transferred and neutered after dispatch.
41
- */
42
- decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
43
- /** Terminates all workers. Rejects all pending and queued jobs. */
44
- dispose(): void;
45
- /** Number of workers in the pool. */
46
- get size(): number;
47
- /** Number of jobs currently queued (waiting for a free worker). */
48
- get queueDepth(): number;
49
- private _dispatch;
50
- private _send;
51
- private _onMessage;
52
- }