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,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
@@ -1,4 +1,4 @@
1
- /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. */
1
+ /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length input. */
2
2
  export declare const hexToBytes: (hex: string) => Uint8Array;
3
3
  /** Uint8Array to lowercase hex string. */
4
4
  export declare const bytesToHex: (bytes: Uint8Array) => string;
@@ -6,26 +6,31 @@ export declare const bytesToHex: (bytes: Uint8Array) => string;
6
6
  export declare const utf8ToBytes: (str: string) => Uint8Array;
7
7
  /** Uint8Array to UTF-8 string. */
8
8
  export declare const bytesToUtf8: (bytes: Uint8Array) => string;
9
- /** Base64 or base64url string to Uint8Array. Returns undefined on invalid input. */
9
+ /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Returns undefined on invalid input. */
10
10
  export declare const base64ToBytes: (b64: string) => Uint8Array | undefined;
11
- /** Uint8Array to base64 string. Pass url=true for base64url encoding. */
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. */
20
24
  export declare const wipe: (data: Uint8Array | Uint16Array | Uint32Array) => void;
21
25
  /** XOR two equal-length Uint8Arrays, returns new array. */
22
26
  export declare const xor: (a: Uint8Array, b: Uint8Array) => Uint8Array;
23
- /** Concatenate two Uint8Arrays, returns new array. */
24
- export declare const concat: (a: Uint8Array, b: Uint8Array) => Uint8Array;
27
+ /** Concatenate one or more Uint8Arrays into a new array. */
28
+ export declare const concat: (...arrays: Uint8Array[]) => Uint8Array;
25
29
  /** Cryptographically secure random bytes via Web Crypto API. */
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
@@ -24,12 +24,12 @@
24
24
  // Pure TypeScript utilities — no init() dependency.
25
25
  // Ported from leviathan/src/base.ts (Convert namespace, Util namespace, constantTimeEqual).
26
26
  // ── Encoding ─────────────────────────────────────────────────────────────────
27
- /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. */
27
+ /** Hex string to Uint8Array. Accepts lowercase/uppercase, optional 0x prefix. Throws RangeError on odd-length input. */
28
28
  export const hexToBytes = (hex) => {
29
29
  if (hex.startsWith('0x') || hex.startsWith('0X'))
30
30
  hex = hex.slice(2);
31
31
  if (hex.length % 2)
32
- hex += '0';
32
+ throw new RangeError(`hexToBytes: odd-length string (${hex.length} chars) — input must be an even-length hex string`);
33
33
  const bin = new Uint8Array(hex.length >>> 1);
34
34
  for (let i = 0, len = hex.length >>> 1; i < len; i++)
35
35
  bin[i] = parseInt(hex.slice(i << 1, (i << 1) + 2), 16);
@@ -51,12 +51,18 @@ export const utf8ToBytes = (str) => {
51
51
  export const bytesToUtf8 = (bytes) => {
52
52
  return new TextDecoder().decode(bytes);
53
53
  };
54
- /** Base64 or base64url string to Uint8Array. Returns undefined on invalid input. */
54
+ /** Base64 or base64url string to Uint8Array. Handles padded, unpadded, and legacy %3d padding. Returns undefined on invalid input. */
55
55
  export const base64ToBytes = (b64) => {
56
56
  // Normalise base64url → base64
57
- b64 = b64.replace(/-/g, '+').replace(/_/g, '/').replace(/%3d/g, '=');
58
- if (b64.length % 4 !== 0)
59
- return undefined;
57
+ b64 = b64.replace(/-/g, '+').replace(/_/g, '/').replace(/%3d/gi, '=');
58
+ // Re-pad if unpadded (RFC 4648 §5 base64url omits '=')
59
+ const rem = b64.length % 4;
60
+ if (rem === 1)
61
+ return undefined; // always invalid — no valid b64 produces this
62
+ if (rem === 2)
63
+ b64 += '==';
64
+ if (rem === 3)
65
+ b64 += '=';
60
66
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64))
61
67
  return undefined;
62
68
  let strlen = b64.length / 4 * 3;
@@ -104,11 +110,11 @@ export const base64ToBytes = (b64) => {
104
110
  }
105
111
  return bin;
106
112
  };
107
- /** Uint8Array to base64 string. Pass url=true for base64url encoding. */
113
+ /** Uint8Array to base64 string. Pass url=true for base64url (RFC 4648 §5 — no padding characters). */
108
114
  export const bytesToBase64 = (bytes, url = false) => {
109
115
  if (typeof btoa !== 'undefined') {
110
116
  const raw = btoa(String.fromCharCode.apply(null, Array.from(bytes)));
111
- return url ? raw.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '%3d') : raw;
117
+ return url ? raw.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') : raw;
112
118
  }
113
119
  // Fallback: manual encode
114
120
  const table = url
@@ -125,20 +131,65 @@ export const bytesToBase64 = (bytes, url = false) => {
125
131
  const triple = (a << 0x10) + (b << 0x08) + c;
126
132
  base64 += table.charAt((triple >>> 18) & 0x3F);
127
133
  base64 += table.charAt((triple >>> 12) & 0x3F);
128
- base64 += (i < bytes.length + 2) ? table.charAt((triple >>> 6) & 0x3F) : (url ? '%3d' : '=');
129
- base64 += (i < bytes.length + 1) ? table.charAt(triple & 0x3F) : (url ? '%3d' : '=');
134
+ base64 += (i < bytes.length + 2) ? table.charAt((triple >>> 6) & 0x3F) : (url ? '' : '=');
135
+ base64 += (i < bytes.length + 1) ? table.charAt(triple & 0x3F) : (url ? '' : '=');
130
136
  }
131
137
  return base64;
132
138
  };
133
- // ── 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
+ }
134
168
  /**
135
169
  * Constant-time byte-array equality.
136
- * 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.
137
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).
138
175
  */
139
176
  export const constantTimeEqual = (a, b) => {
140
177
  if (a.length !== b.length)
141
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
142
193
  let diff = 0;
143
194
  for (let i = 0; i < a.length; i++)
144
195
  diff |= a[i] ^ b[i];
@@ -154,12 +205,16 @@ export const xor = (a, b) => {
154
205
  throw new RangeError(`xor: length mismatch (${a.length} vs ${b.length})`);
155
206
  return a.map((val, i) => val ^ b[i]);
156
207
  };
157
- /** Concatenate two Uint8Arrays, returns new array. */
158
- export const concat = (a, b) => {
159
- const result = new Uint8Array(a.length + b.length);
160
- result.set(a, 0);
161
- result.set(b, a.length);
162
- return result;
208
+ /** Concatenate one or more Uint8Arrays into a new array. */
209
+ export const concat = (...arrays) => {
210
+ const len = arrays.reduce((s, a) => s + a.length, 0);
211
+ const out = new Uint8Array(len);
212
+ let off = 0;
213
+ for (const a of arrays) {
214
+ out.set(a, off);
215
+ off += a.length;
216
+ }
217
+ return out;
163
218
  };
164
219
  /** Cryptographically secure random bytes via Web Crypto API. */
165
220
  export const randomBytes = (n) => {
@@ -171,7 +226,8 @@ export const randomBytes = (n) => {
171
226
  let _simd = null;
172
227
  /**
173
228
  * Detects WASM SIMD support once and caches the result.
174
- * 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().
175
231
  */
176
232
  export function hasSIMD() {
177
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.3.1",
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
- }