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
@@ -1,28 +0,0 @@
1
- import { SerpentCtr } from './index.js';
2
- import { HMAC_SHA256, HKDF_SHA256 } from '../sha2/index.js';
3
- export declare function u32be(n: number): Uint8Array;
4
- export declare function u64be(n: number): Uint8Array;
5
- export declare function chunkInfo(streamNonce: Uint8Array, chunkSize: number, chunkCount: number, index: number, isLast: boolean): Uint8Array;
6
- export declare function deriveChunkKeys(hkdf: HKDF_SHA256, masterKey: Uint8Array, streamNonce: Uint8Array, chunkSize: number, chunkCount: number, index: number, isLast: boolean): {
7
- encKey: Uint8Array;
8
- macKey: Uint8Array;
9
- };
10
- /**
11
- * Encrypt one chunk. Returns ciphertext || hmac_tag (32 bytes).
12
- * Does not generate keys -- caller provides encKey and macKey.
13
- */
14
- export declare function sealChunk(ctr: SerpentCtr, hmac: HMAC_SHA256, encKey: Uint8Array, macKey: Uint8Array, chunk: Uint8Array): Uint8Array;
15
- /**
16
- * Decrypt one chunk. Throws 'SerpentStream: authentication failed' on bad tag.
17
- * Returns plaintext.
18
- */
19
- export declare function openChunk(ctr: SerpentCtr, hmac: HMAC_SHA256, encKey: Uint8Array, macKey: Uint8Array, wire: Uint8Array): Uint8Array;
20
- export declare class SerpentStream {
21
- private readonly _ctr;
22
- private readonly _hmac;
23
- private readonly _hkdf;
24
- constructor();
25
- seal(key: Uint8Array, plaintext: Uint8Array, chunkSize?: number, _nonce?: Uint8Array): Uint8Array;
26
- open(key: Uint8Array, ciphertext: Uint8Array): Uint8Array;
27
- dispose(): void;
28
- }
@@ -1,205 +0,0 @@
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/stream.ts
23
- //
24
- // SerpentStream — chunked authenticated encryption for large payloads.
25
- // Tier 2 pure-TS composition: SerpentCtr + HMAC_SHA256 + HKDF_SHA256.
26
- import { SerpentCtr, _serpentReady } from './index.js';
27
- import { HMAC_SHA256, HKDF_SHA256, _sha2Ready } from '../sha2/index.js';
28
- import { constantTimeEqual } from '../utils.js';
29
- // ── Constants ─────────────────────────────────────────────────────────────────
30
- const DOMAIN = 'serpent-stream-v1'; // UTF-8, 17 bytes
31
- const ZERO_IV = new Uint8Array(16); // fixed zero IV for CTR
32
- const CHUNK_MIN = 1024; // 1 KB
33
- const CHUNK_MAX = 65536; // 64 KB
34
- const CHUNK_DEF = 65536; // default
35
- // ── Internal helpers ──────────────────────────────────────────────────────────
36
- export function u32be(n) {
37
- const b = new Uint8Array(4);
38
- b[0] = (n >>> 24) & 0xff;
39
- b[1] = (n >>> 16) & 0xff;
40
- b[2] = (n >>> 8) & 0xff;
41
- b[3] = n & 0xff;
42
- return b;
43
- }
44
- export function u64be(n) {
45
- const b = new Uint8Array(8);
46
- // high 32 bits (safe for n < 2^53)
47
- const hi = Math.floor(n / 0x100000000);
48
- const lo = n >>> 0;
49
- b[0] = (hi >>> 24) & 0xff;
50
- b[1] = (hi >>> 16) & 0xff;
51
- b[2] = (hi >>> 8) & 0xff;
52
- b[3] = hi & 0xff;
53
- b[4] = (lo >>> 24) & 0xff;
54
- b[5] = (lo >>> 16) & 0xff;
55
- b[6] = (lo >>> 8) & 0xff;
56
- b[7] = lo & 0xff;
57
- return b;
58
- }
59
- const DOMAIN_BYTES = new TextEncoder().encode(DOMAIN);
60
- export function chunkInfo(streamNonce, chunkSize, chunkCount, index, isLast) {
61
- // 17 + 16 + 4 + 8 + 8 + 1 = 54 bytes
62
- const info = new Uint8Array(54);
63
- let off = 0;
64
- info.set(DOMAIN_BYTES, off);
65
- off += 17;
66
- info.set(streamNonce, off);
67
- off += 16;
68
- info.set(u32be(chunkSize), off);
69
- off += 4;
70
- info.set(u64be(chunkCount), off);
71
- off += 8;
72
- info.set(u64be(index), off);
73
- off += 8;
74
- info[off] = isLast ? 0x01 : 0x00;
75
- return info;
76
- }
77
- export function deriveChunkKeys(hkdf, masterKey, streamNonce, chunkSize, chunkCount, index, isLast) {
78
- const info = chunkInfo(streamNonce, chunkSize, chunkCount, index, isLast);
79
- const derived = hkdf.derive(masterKey, streamNonce, info, 64);
80
- return {
81
- encKey: derived.subarray(0, 32),
82
- macKey: derived.subarray(32, 64),
83
- };
84
- }
85
- // ── Exported chunk-level ops (used by Part 2 pool worker) ─────────────────────
86
- /**
87
- * Encrypt one chunk. Returns ciphertext || hmac_tag (32 bytes).
88
- * Does not generate keys -- caller provides encKey and macKey.
89
- */
90
- export function sealChunk(ctr, hmac, encKey, macKey, chunk) {
91
- ctr.beginEncrypt(encKey, ZERO_IV);
92
- const ciphertext = ctr.encryptChunk(chunk);
93
- const tag = hmac.hash(macKey, ciphertext);
94
- const out = new Uint8Array(ciphertext.length + 32);
95
- out.set(ciphertext, 0);
96
- out.set(tag, ciphertext.length);
97
- return out;
98
- }
99
- /**
100
- * Decrypt one chunk. Throws 'SerpentStream: authentication failed' on bad tag.
101
- * Returns plaintext.
102
- */
103
- export function openChunk(ctr, hmac, encKey, macKey, wire) {
104
- if (wire.length < 32)
105
- throw new RangeError('SerpentStream: chunk wire data too short');
106
- const ciphertext = wire.subarray(0, wire.length - 32);
107
- const tag = wire.subarray(wire.length - 32);
108
- const expectedTag = hmac.hash(macKey, ciphertext);
109
- if (!constantTimeEqual(tag, expectedTag))
110
- throw new Error('SerpentStream: authentication failed');
111
- ctr.beginEncrypt(encKey, ZERO_IV);
112
- return ctr.encryptChunk(ciphertext);
113
- }
114
- // ── SerpentStream class ───────────────────────────────────────────────────────
115
- export class SerpentStream {
116
- _ctr;
117
- _hmac;
118
- _hkdf;
119
- constructor() {
120
- if (!_serpentReady() || !_sha2Ready())
121
- throw new Error('leviathan-crypto: call init([\'serpent\', \'sha2\']) before using SerpentStream');
122
- this._ctr = new SerpentCtr({ dangerUnauthenticated: true });
123
- this._hmac = new HMAC_SHA256();
124
- this._hkdf = new HKDF_SHA256();
125
- }
126
- // _nonce: test seam only — inject a fixed nonce for deterministic KAT vectors
127
- seal(key, plaintext, chunkSize, _nonce) {
128
- if (key.length !== 32)
129
- throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
130
- const cs = chunkSize ?? CHUNK_DEF;
131
- if (cs < CHUNK_MIN || cs > CHUNK_MAX)
132
- throw new RangeError(`SerpentStream chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
133
- const streamNonce = (_nonce && _nonce.length === 16) ? _nonce : new Uint8Array(16);
134
- if (!_nonce || _nonce.length !== 16)
135
- crypto.getRandomValues(streamNonce);
136
- const chunkCount = plaintext.length === 0 ? 1 : Math.ceil(plaintext.length / cs);
137
- // Compute total output size
138
- let totalWire = 28; // header
139
- for (let i = 0; i < chunkCount; i++) {
140
- const start = i * cs;
141
- const end = Math.min(start + cs, plaintext.length);
142
- totalWire += (end - start) + 32;
143
- }
144
- const out = new Uint8Array(totalWire);
145
- // Write header
146
- out.set(streamNonce, 0);
147
- out.set(u32be(cs), 16);
148
- out.set(u64be(chunkCount), 20);
149
- let pos = 28;
150
- for (let i = 0; i < chunkCount; i++) {
151
- const start = i * cs;
152
- const end = Math.min(start + cs, plaintext.length);
153
- const slice = plaintext.subarray(start, end);
154
- const isLast = i === chunkCount - 1;
155
- const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
156
- const wire = sealChunk(this._ctr, this._hmac, encKey, macKey, slice);
157
- out.set(wire, pos);
158
- pos += wire.length;
159
- }
160
- return out;
161
- }
162
- open(key, ciphertext) {
163
- if (key.length !== 32)
164
- throw new RangeError(`SerpentStream key must be 32 bytes (got ${key.length})`);
165
- if (ciphertext.length < 28 + 32)
166
- throw new RangeError('SerpentStream: ciphertext too short');
167
- // Parse header
168
- const streamNonce = ciphertext.subarray(0, 16);
169
- const csView = ciphertext.subarray(16, 20);
170
- const cs = (csView[0] << 24) | (csView[1] << 16) | (csView[2] << 8) | csView[3];
171
- const ccView = ciphertext.subarray(20, 28);
172
- let chunkCount = 0;
173
- for (let i = 0; i < 8; i++)
174
- chunkCount = chunkCount * 256 + ccView[i];
175
- // Compute total plaintext size
176
- let totalPt = 0;
177
- let pos = 28;
178
- for (let i = 0; i < chunkCount; i++) {
179
- const isLast = i === chunkCount - 1;
180
- const wireLen = isLast ? ciphertext.length - pos : cs + 32;
181
- totalPt += wireLen - 32;
182
- if (!isLast)
183
- pos += wireLen;
184
- }
185
- const plaintext = new Uint8Array(totalPt);
186
- pos = 28;
187
- let ptPos = 0;
188
- for (let i = 0; i < chunkCount; i++) {
189
- const isLast = i === chunkCount - 1;
190
- const wireLen = isLast ? ciphertext.length - pos : cs + 32;
191
- const wireSlice = ciphertext.subarray(pos, pos + wireLen);
192
- const { encKey, macKey } = deriveChunkKeys(this._hkdf, key, streamNonce, cs, chunkCount, i, isLast);
193
- const pt = openChunk(this._ctr, this._hmac, encKey, macKey, wireSlice);
194
- plaintext.set(pt, ptPos);
195
- ptPos += pt.length;
196
- pos += wireLen;
197
- }
198
- return plaintext;
199
- }
200
- dispose() {
201
- this._ctr.dispose();
202
- this._hmac.dispose();
203
- this._hkdf.dispose();
204
- }
205
- }
@@ -1,32 +0,0 @@
1
- interface SerpentExports {
2
- memory: WebAssembly.Memory;
3
- getKeyOffset: () => number;
4
- getNonceOffset: () => number;
5
- getChunkPtOffset: () => number;
6
- getChunkCtOffset: () => number;
7
- getChunkSize: () => number;
8
- loadKey: (n: number) => number;
9
- resetCounter: () => void;
10
- encryptChunk: (n: number) => number;
11
- wipeBuffers: () => void;
12
- }
13
- interface Sha2Exports {
14
- memory: WebAssembly.Memory;
15
- getSha256InputOffset: () => number;
16
- getSha256OutOffset: () => number;
17
- sha256Init: () => void;
18
- sha256Update: (len: number) => void;
19
- sha256Final: () => void;
20
- hmac256Init: (keyLen: number) => void;
21
- hmac256Update: (len: number) => void;
22
- hmac256Final: () => void;
23
- wipeBuffers: () => void;
24
- }
25
- declare let sx: SerpentExports | undefined;
26
- declare let hx: Sha2Exports | undefined;
27
- declare const ZERO_IV: Uint8Array<ArrayBuffer>;
28
- declare function hmacSha256(hx: Sha2Exports, key: Uint8Array, msg: Uint8Array): Uint8Array;
29
- declare function ctrEncrypt(sx: SerpentExports, key: Uint8Array, chunk: Uint8Array): Uint8Array;
30
- declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
31
- declare function workerSealChunk(encKey: Uint8Array, macKey: Uint8Array, chunk: Uint8Array): Uint8Array;
32
- declare function workerOpenChunk(encKey: Uint8Array, macKey: Uint8Array, wire: Uint8Array): Uint8Array;
@@ -1,117 +0,0 @@
1
- "use strict";
2
- // / <reference lib="webworker" />
3
- // src/ts/serpent/stream.worker.ts
4
- //
5
- // Worker entry point for SerpentStreamPool. Runs in a Web Worker —
6
- // no access to the main thread's module cache. Owns its own
7
- // serpent.wasm and sha2.wasm instances with isolated linear memory.
8
- // Implements sealChunk/openChunk inline using raw WASM exports.
9
- let sx;
10
- let hx;
11
- const ZERO_IV = new Uint8Array(16);
12
- // ── Inline chunk ops ──────────────────────────────────────────────────────────
13
- function hmacSha256(hx, key, msg) {
14
- // RFC 2104 §3: keys longer than block size (64 bytes) are pre-hashed.
15
- // mac_key is always 32 bytes in normal usage (half of HKDF 64-byte output),
16
- // but this guard must match the main-thread HMAC_SHA256.hash() behaviour
17
- // exactly — any divergence would cause authentication failures if key sizes
18
- // ever change.
19
- let k = key;
20
- if (k.length > 64) {
21
- hx.sha256Init();
22
- let pos = 0;
23
- while (pos < k.length) {
24
- const n = Math.min(k.length - pos, 64);
25
- new Uint8Array(hx.memory.buffer).set(k.subarray(pos, pos + n), hx.getSha256InputOffset());
26
- hx.sha256Update(n);
27
- pos += n;
28
- }
29
- hx.sha256Final();
30
- const out = new Uint8Array(hx.memory.buffer);
31
- k = out.slice(hx.getSha256OutOffset(), hx.getSha256OutOffset() + 32);
32
- }
33
- const mem = new Uint8Array(hx.memory.buffer);
34
- const inputOff = hx.getSha256InputOffset();
35
- mem.set(k, inputOff);
36
- hx.hmac256Init(k.length);
37
- let pos = 0;
38
- while (pos < msg.length) {
39
- const n = Math.min(msg.length - pos, 64);
40
- new Uint8Array(hx.memory.buffer).set(msg.subarray(pos, pos + n), inputOff);
41
- hx.hmac256Update(n);
42
- pos += n;
43
- }
44
- hx.hmac256Final();
45
- const out = new Uint8Array(hx.memory.buffer);
46
- return out.slice(hx.getSha256OutOffset(), hx.getSha256OutOffset() + 32);
47
- }
48
- function ctrEncrypt(sx, key, chunk) {
49
- const mem = new Uint8Array(sx.memory.buffer);
50
- mem.set(key, sx.getKeyOffset());
51
- mem.set(ZERO_IV, sx.getNonceOffset());
52
- sx.loadKey(key.length);
53
- sx.resetCounter();
54
- new Uint8Array(sx.memory.buffer).set(chunk, sx.getChunkPtOffset());
55
- sx.encryptChunk(chunk.length);
56
- const out = new Uint8Array(sx.memory.buffer);
57
- return out.slice(sx.getChunkCtOffset(), sx.getChunkCtOffset() + chunk.length);
58
- }
59
- function constantTimeEqual(a, b) {
60
- if (a.length !== b.length)
61
- return false;
62
- let diff = 0;
63
- for (let i = 0; i < a.length; i++)
64
- diff |= a[i] ^ b[i];
65
- return diff === 0;
66
- }
67
- function workerSealChunk(encKey, macKey, chunk) {
68
- const ciphertext = ctrEncrypt(sx, encKey, chunk);
69
- const tag = hmacSha256(hx, macKey, ciphertext);
70
- const out = new Uint8Array(ciphertext.length + 32);
71
- out.set(ciphertext, 0);
72
- out.set(tag, ciphertext.length);
73
- return out;
74
- }
75
- function workerOpenChunk(encKey, macKey, wire) {
76
- if (wire.length < 32)
77
- throw new RangeError('SerpentStream: chunk wire data too short');
78
- const ciphertext = wire.subarray(0, wire.length - 32);
79
- const tag = wire.subarray(wire.length - 32);
80
- const expectedTag = hmacSha256(hx, macKey, ciphertext);
81
- if (!constantTimeEqual(tag, expectedTag))
82
- throw new Error('SerpentStream: authentication failed');
83
- return ctrEncrypt(sx, encKey, ciphertext);
84
- }
85
- // ── Message handler ───────────────────────────────────────────────────────────
86
- self.onmessage = async (e) => {
87
- const msg = e.data;
88
- if (msg.type === 'init') {
89
- try {
90
- const serpentMem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
91
- const sha2Mem = new WebAssembly.Memory({ initial: 3, maximum: 3 });
92
- const serpentInst = await WebAssembly.instantiate(msg.serpentModule, { env: { memory: serpentMem } });
93
- const sha2Inst = await WebAssembly.instantiate(msg.sha2Module, { env: { memory: sha2Mem } });
94
- sx = serpentInst.exports;
95
- hx = sha2Inst.exports;
96
- self.postMessage({ type: 'ready' });
97
- }
98
- catch (err) {
99
- self.postMessage({ type: 'error', id: -1, message: err.message });
100
- }
101
- return;
102
- }
103
- if (!sx || !hx) {
104
- self.postMessage({ type: 'error', id: msg.id, message: 'worker not initialized' });
105
- return;
106
- }
107
- try {
108
- const { id, op, encKey, macKey, data } = msg;
109
- const result = op === 'seal'
110
- ? workerSealChunk(encKey, macKey, data)
111
- : workerOpenChunk(encKey, macKey, data);
112
- self.postMessage({ type: 'result', id, data: result }, [result.buffer]);
113
- }
114
- catch (err) {
115
- self.postMessage({ type: 'error', id: msg.id, message: err.message });
116
- }
117
- };