@sebastienrousseau/crypto-lib 0.0.9 → 0.0.24
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.
- package/dist/accel/wasm-bridge.d.ts.map +1 -1
- package/dist/accel/wasm-bridge.js +46 -46
- package/dist/high-level/index.d.ts +1 -1
- package/dist/high-level/index.d.ts.map +1 -1
- package/dist/high-level/index.js +3 -1
- package/dist/high-level/multi-recipient.d.ts +6 -0
- package/dist/high-level/multi-recipient.d.ts.map +1 -1
- package/dist/high-level/multi-recipient.js +34 -23
- package/dist/modern/hash.d.ts.map +1 -1
- package/dist/modern/hash.js +26 -14
- package/dist/modern/hpke.d.ts +1 -1
- package/dist/modern/hpke.d.ts.map +1 -1
- package/dist/modern/hpke.js +74 -11
- package/dist/modern/index.d.ts +1 -1
- package/dist/modern/index.d.ts.map +1 -1
- package/dist/modern/index.js +2 -0
- package/dist/modern/kdf.d.ts.map +1 -1
- package/dist/modern/kdf.js +25 -19
- package/dist/streaming/index.d.ts +6 -2
- package/dist/streaming/index.d.ts.map +1 -1
- package/dist/streaming/index.js +18 -1
- package/dist/streaming/stream-multi-pq-aead.d.ts +35 -0
- package/dist/streaming/stream-multi-pq-aead.d.ts.map +1 -0
- package/dist/streaming/stream-multi-pq-aead.js +315 -0
- package/dist/streaming/stream-pq-aead.d.ts +29 -0
- package/dist/streaming/stream-pq-aead.d.ts.map +1 -0
- package/dist/streaming/stream-pq-aead.js +175 -0
- package/dist/streaming/web-streams.d.ts +25 -0
- package/dist/streaming/web-streams.d.ts.map +1 -1
- package/dist/streaming/web-streams.js +106 -0
- package/package.json +2 -2
package/dist/streaming/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WEB_STREAM_HASH_ALGORITHMS = exports.createHashStream = exports.createDecryptStream = exports.createEncryptStream = exports.streamDecrypt = exports.streamEncrypt = exports.STREAM_HASH_ALGORITHMS = exports.createHasher = void 0;
|
|
3
|
+
exports.SLOT_FIXED_OVERHEAD = exports.WRAPPED_CEK_LEN = exports.CEK_LEN = exports.MULTI_PQ_PREFIX_LEN = exports.MULTI_PQ_STREAM_VERSION = exports.MULTI_PQ_STREAM_MAGIC = exports.streamMultiPqDecrypt = exports.streamMultiPqEncrypt = exports.PQ_STREAM_HEADER_LEN = exports.streamPqDecrypt = exports.streamPqEncrypt = exports.WEB_STREAM_HASH_ALGORITHMS = exports.createMultiPqDecryptStream = exports.createMultiPqEncryptStream = exports.createPqDecryptStream = exports.createPqEncryptStream = exports.createHashStream = exports.createDecryptStream = exports.createEncryptStream = exports.streamDecrypt = exports.streamEncrypt = exports.STREAM_HASH_ALGORITHMS = exports.createHasher = void 0;
|
|
4
4
|
var stream_hash_1 = require("./stream-hash");
|
|
5
5
|
Object.defineProperty(exports, "createHasher", { enumerable: true, get: function () { return stream_hash_1.createHasher; } });
|
|
6
6
|
Object.defineProperty(exports, "STREAM_HASH_ALGORITHMS", { enumerable: true, get: function () { return stream_hash_1.STREAM_HASH_ALGORITHMS; } });
|
|
@@ -11,5 +11,22 @@ var web_streams_1 = require("./web-streams");
|
|
|
11
11
|
Object.defineProperty(exports, "createEncryptStream", { enumerable: true, get: function () { return web_streams_1.createEncryptStream; } });
|
|
12
12
|
Object.defineProperty(exports, "createDecryptStream", { enumerable: true, get: function () { return web_streams_1.createDecryptStream; } });
|
|
13
13
|
Object.defineProperty(exports, "createHashStream", { enumerable: true, get: function () { return web_streams_1.createHashStream; } });
|
|
14
|
+
Object.defineProperty(exports, "createPqEncryptStream", { enumerable: true, get: function () { return web_streams_1.createPqEncryptStream; } });
|
|
15
|
+
Object.defineProperty(exports, "createPqDecryptStream", { enumerable: true, get: function () { return web_streams_1.createPqDecryptStream; } });
|
|
16
|
+
Object.defineProperty(exports, "createMultiPqEncryptStream", { enumerable: true, get: function () { return web_streams_1.createMultiPqEncryptStream; } });
|
|
17
|
+
Object.defineProperty(exports, "createMultiPqDecryptStream", { enumerable: true, get: function () { return web_streams_1.createMultiPqDecryptStream; } });
|
|
14
18
|
Object.defineProperty(exports, "WEB_STREAM_HASH_ALGORITHMS", { enumerable: true, get: function () { return web_streams_1.WEB_STREAM_HASH_ALGORITHMS; } });
|
|
19
|
+
var stream_pq_aead_1 = require("./stream-pq-aead");
|
|
20
|
+
Object.defineProperty(exports, "streamPqEncrypt", { enumerable: true, get: function () { return stream_pq_aead_1.streamPqEncrypt; } });
|
|
21
|
+
Object.defineProperty(exports, "streamPqDecrypt", { enumerable: true, get: function () { return stream_pq_aead_1.streamPqDecrypt; } });
|
|
22
|
+
Object.defineProperty(exports, "PQ_STREAM_HEADER_LEN", { enumerable: true, get: function () { return stream_pq_aead_1.PQ_STREAM_HEADER_LEN; } });
|
|
23
|
+
var stream_multi_pq_aead_1 = require("./stream-multi-pq-aead");
|
|
24
|
+
Object.defineProperty(exports, "streamMultiPqEncrypt", { enumerable: true, get: function () { return stream_multi_pq_aead_1.streamMultiPqEncrypt; } });
|
|
25
|
+
Object.defineProperty(exports, "streamMultiPqDecrypt", { enumerable: true, get: function () { return stream_multi_pq_aead_1.streamMultiPqDecrypt; } });
|
|
26
|
+
Object.defineProperty(exports, "MULTI_PQ_STREAM_MAGIC", { enumerable: true, get: function () { return stream_multi_pq_aead_1.MULTI_PQ_STREAM_MAGIC; } });
|
|
27
|
+
Object.defineProperty(exports, "MULTI_PQ_STREAM_VERSION", { enumerable: true, get: function () { return stream_multi_pq_aead_1.MULTI_PQ_STREAM_VERSION; } });
|
|
28
|
+
Object.defineProperty(exports, "MULTI_PQ_PREFIX_LEN", { enumerable: true, get: function () { return stream_multi_pq_aead_1.MULTI_PQ_PREFIX_LEN; } });
|
|
29
|
+
Object.defineProperty(exports, "CEK_LEN", { enumerable: true, get: function () { return stream_multi_pq_aead_1.CEK_LEN; } });
|
|
30
|
+
Object.defineProperty(exports, "WRAPPED_CEK_LEN", { enumerable: true, get: function () { return stream_multi_pq_aead_1.WRAPPED_CEK_LEN; } });
|
|
31
|
+
Object.defineProperty(exports, "SLOT_FIXED_OVERHEAD", { enumerable: true, get: function () { return stream_multi_pq_aead_1.SLOT_FIXED_OVERHEAD; } });
|
|
15
32
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const MULTI_PQ_STREAM_MAGIC: Uint8Array<ArrayBuffer>;
|
|
2
|
+
export declare const MULTI_PQ_STREAM_VERSION = 1;
|
|
3
|
+
export declare const MULTI_PQ_PREFIX_LEN = 35;
|
|
4
|
+
export declare const CEK_LEN = 32;
|
|
5
|
+
export declare const WRAPPED_CEK_LEN: number;
|
|
6
|
+
export declare const SLOT_FIXED_OVERHEAD: number;
|
|
7
|
+
export interface MultiPqRecipient {
|
|
8
|
+
recipientId: string;
|
|
9
|
+
recipientX25519Public: string | Uint8Array;
|
|
10
|
+
recipientMlKemPublic: string | Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
export interface StreamMultiPqEncryptOptions {
|
|
13
|
+
recipients: MultiPqRecipient[];
|
|
14
|
+
plaintext: Uint8Array;
|
|
15
|
+
chunkSize?: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface StreamMultiPqEncryptResult {
|
|
18
|
+
ciphertext: Uint8Array;
|
|
19
|
+
algorithm: "multi-x25519-ml-kem-768-xchacha20-poly1305-stream";
|
|
20
|
+
recipientCount: number;
|
|
21
|
+
}
|
|
22
|
+
export interface StreamMultiPqDecryptOptions {
|
|
23
|
+
recipientId?: string | undefined;
|
|
24
|
+
recipientX25519Secret: string | Uint8Array;
|
|
25
|
+
recipientMlKemSecret: string | Uint8Array;
|
|
26
|
+
ciphertext: Uint8Array;
|
|
27
|
+
chunkSize?: number | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface StreamMultiPqDecryptResult {
|
|
30
|
+
plaintext: Uint8Array;
|
|
31
|
+
recipientId: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function streamMultiPqEncrypt(options: StreamMultiPqEncryptOptions): StreamMultiPqEncryptResult;
|
|
34
|
+
export declare function streamMultiPqDecrypt(options: StreamMultiPqDecryptOptions): StreamMultiPqDecryptResult;
|
|
35
|
+
//# sourceMappingURL=stream-multi-pq-aead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-multi-pq-aead.d.ts","sourceRoot":"","sources":["../../src/streaming/stream-multi-pq-aead.ts"],"names":[],"mappings":"AAoCA,eAAO,MAAM,qBAAqB,yBAA2C,CAAC;AAG9E,eAAO,MAAM,uBAAuB,IAAO,CAAC;AAG5C,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAGtC,eAAO,MAAM,OAAO,KAAK,CAAC;AAG1B,eAAO,MAAM,eAAe,QAAoB,CAAC;AAGjD,eAAO,MAAM,mBAAmB,QACiC,CAAC;AAOlE,MAAM,WAAW,gBAAgB;IAE/B,WAAW,EAAE,MAAM,CAAC;IAEpB,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3C;AAGD,MAAM,WAAW,2BAA2B;IAE1C,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAE/B,SAAS,EAAE,UAAU,CAAC;IAEtB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,0BAA0B;IAEzC,UAAU,EAAE,UAAU,CAAC;IAEvB,SAAS,EAAE,mDAAmD,CAAC;IAE/D,cAAc,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,2BAA2B;IAE1C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE1C,UAAU,EAAE,UAAU,CAAC;IAEvB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,0BAA0B;IAEzC,SAAS,EAAE,UAAU,CAAC;IAEtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgLD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,0BAA0B,CAsD5B;AAwND,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,0BAA0B,CAyC5B"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SLOT_FIXED_OVERHEAD = exports.WRAPPED_CEK_LEN = exports.CEK_LEN = exports.MULTI_PQ_PREFIX_LEN = exports.MULTI_PQ_STREAM_VERSION = exports.MULTI_PQ_STREAM_MAGIC = void 0;
|
|
4
|
+
exports.streamMultiPqEncrypt = streamMultiPqEncrypt;
|
|
5
|
+
exports.streamMultiPqDecrypt = streamMultiPqDecrypt;
|
|
6
|
+
const ed25519_js_1 = require("@noble/curves/ed25519.js");
|
|
7
|
+
const ml_kem_js_1 = require("@noble/post-quantum/ml-kem.js");
|
|
8
|
+
const hkdf_js_1 = require("@noble/hashes/hkdf.js");
|
|
9
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
10
|
+
const chacha_js_1 = require("@noble/ciphers/chacha.js");
|
|
11
|
+
const utils_js_1 = require("@noble/ciphers/utils.js");
|
|
12
|
+
const utils_1 = require("../utils");
|
|
13
|
+
const stream_pq_aead_1 = require("./stream-pq-aead");
|
|
14
|
+
exports.MULTI_PQ_STREAM_MAGIC = new Uint8Array([0x4d, 0x53, 0x50, 0x51]);
|
|
15
|
+
exports.MULTI_PQ_STREAM_VERSION = 0x01;
|
|
16
|
+
exports.MULTI_PQ_PREFIX_LEN = 35;
|
|
17
|
+
exports.CEK_LEN = 32;
|
|
18
|
+
exports.WRAPPED_CEK_LEN = exports.CEK_LEN + stream_pq_aead_1.TAG_LEN;
|
|
19
|
+
exports.SLOT_FIXED_OVERHEAD = 1 + stream_pq_aead_1.EPHEMERAL_LEN + stream_pq_aead_1.ML_KEM_CT_LEN + stream_pq_aead_1.NONCE_LEN + exports.WRAPPED_CEK_LEN;
|
|
20
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
21
|
+
const TEXT_DECODER = new TextDecoder();
|
|
22
|
+
const KEK_INFO = TEXT_ENCODER.encode("multi-pq-stream-kek-v1");
|
|
23
|
+
function validateRecipient(r) {
|
|
24
|
+
if (!r.recipientId || typeof r.recipientId !== "string") {
|
|
25
|
+
throw new Error("Recipient ID must be a non-empty string");
|
|
26
|
+
}
|
|
27
|
+
const idBytes = TEXT_ENCODER.encode(r.recipientId);
|
|
28
|
+
if (idBytes.length === 0 || idBytes.length > 255) {
|
|
29
|
+
throw new Error("Recipient ID must be between 1 and 255 UTF-8 bytes");
|
|
30
|
+
}
|
|
31
|
+
const x25519Pub = (0, stream_pq_aead_1.toBytes)(r.recipientX25519Public, 32, "recipientX25519Public");
|
|
32
|
+
const mlKemPub = (0, stream_pq_aead_1.toBytes)(r.recipientMlKemPublic, 1184, "recipientMlKemPublic");
|
|
33
|
+
return { idBytes, x25519Pub, mlKemPub };
|
|
34
|
+
}
|
|
35
|
+
function createSlot(cek, slotIndex, r) {
|
|
36
|
+
const { idBytes, x25519Pub, mlKemPub } = validateRecipient(r);
|
|
37
|
+
const ephPriv = (0, utils_js_1.randomBytes)(32);
|
|
38
|
+
const ephPub = ed25519_js_1.x25519.getPublicKey(ephPriv);
|
|
39
|
+
const x25519Shared = ed25519_js_1.x25519.getSharedSecret(ephPriv, x25519Pub);
|
|
40
|
+
const { cipherText: mlKemCt, sharedSecret: mlKemShared } = ml_kem_js_1.ml_kem768.encapsulate(mlKemPub);
|
|
41
|
+
const combined = new Uint8Array(64);
|
|
42
|
+
combined.set(x25519Shared, 0);
|
|
43
|
+
combined.set(mlKemShared, 32);
|
|
44
|
+
const kek = (0, hkdf_js_1.hkdf)(sha2_js_1.sha256, combined, ephPub, KEK_INFO, 32);
|
|
45
|
+
const slotNonce = (0, utils_js_1.randomBytes)(stream_pq_aead_1.NONCE_LEN);
|
|
46
|
+
const aad = buildSlotAad(idBytes, slotIndex);
|
|
47
|
+
const wrappedCek = (0, chacha_js_1.xchacha20poly1305)(kek, slotNonce, aad).encrypt(cek);
|
|
48
|
+
(0, utils_1.wipeMemory)(ephPriv);
|
|
49
|
+
(0, utils_1.wipeMemory)(x25519Shared);
|
|
50
|
+
(0, utils_1.wipeMemory)(mlKemShared);
|
|
51
|
+
(0, utils_1.wipeMemory)(combined);
|
|
52
|
+
(0, utils_1.wipeMemory)(kek);
|
|
53
|
+
return serializeSlot(idBytes, ephPub, mlKemCt, slotNonce, wrappedCek);
|
|
54
|
+
}
|
|
55
|
+
function buildSlotAad(idBytes, slotIndex) {
|
|
56
|
+
const aad = new Uint8Array(idBytes.length + 4);
|
|
57
|
+
aad.set(idBytes, 0);
|
|
58
|
+
const view = new DataView(aad.buffer, aad.byteOffset, aad.byteLength);
|
|
59
|
+
view.setUint32(idBytes.length, slotIndex, false);
|
|
60
|
+
return aad;
|
|
61
|
+
}
|
|
62
|
+
function serializeSlot(idBytes, ephPub, mlKemCt, slotNonce, wrappedCek) {
|
|
63
|
+
const slotLen = exports.SLOT_FIXED_OVERHEAD + idBytes.length;
|
|
64
|
+
const out = new Uint8Array(slotLen);
|
|
65
|
+
out[0] = idBytes.length;
|
|
66
|
+
let pos = 1;
|
|
67
|
+
out.set(idBytes, pos);
|
|
68
|
+
pos += idBytes.length;
|
|
69
|
+
out.set(ephPub, pos);
|
|
70
|
+
pos += stream_pq_aead_1.EPHEMERAL_LEN;
|
|
71
|
+
out.set(mlKemCt, pos);
|
|
72
|
+
pos += stream_pq_aead_1.ML_KEM_CT_LEN;
|
|
73
|
+
out.set(slotNonce, pos);
|
|
74
|
+
pos += stream_pq_aead_1.NONCE_LEN;
|
|
75
|
+
out.set(wrappedCek, pos);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function computeMultiStreamOutputSize(headerLen, ptLen, chunkSize) {
|
|
79
|
+
const numFullChunks = Math.floor(ptLen / chunkSize);
|
|
80
|
+
const lastChunkLen = ptLen - numFullChunks * chunkSize;
|
|
81
|
+
const hasExtra = lastChunkLen > 0 || ptLen === 0;
|
|
82
|
+
const totalChunks = hasExtra ? numFullChunks + 1 : numFullChunks;
|
|
83
|
+
let totalSize = headerLen;
|
|
84
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
85
|
+
const len = i < numFullChunks ? chunkSize : lastChunkLen > 0 ? lastChunkLen : 0;
|
|
86
|
+
totalSize += 1 + len + stream_pq_aead_1.TAG_LEN;
|
|
87
|
+
}
|
|
88
|
+
return totalSize;
|
|
89
|
+
}
|
|
90
|
+
function encryptStreamChunks(plaintext, cek, baseNonce, chunkSize, output, headerLen) {
|
|
91
|
+
const numFullChunks = Math.floor(plaintext.length / chunkSize);
|
|
92
|
+
const hasExtra = plaintext.length % chunkSize > 0 || plaintext.length === 0;
|
|
93
|
+
const totalChunks = hasExtra ? numFullChunks + 1 : numFullChunks;
|
|
94
|
+
let offset = headerLen;
|
|
95
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
96
|
+
const isFinal = i === totalChunks - 1;
|
|
97
|
+
const chunkTag = isFinal ? stream_pq_aead_1.CHUNK_TAG_FINAL : stream_pq_aead_1.CHUNK_TAG_MESSAGE;
|
|
98
|
+
const start = i * chunkSize;
|
|
99
|
+
const end = isFinal ? plaintext.length : start + chunkSize;
|
|
100
|
+
const chunk = plaintext.subarray(start, end);
|
|
101
|
+
const nonce = (0, stream_pq_aead_1.derivePqChunkNonce)(baseNonce, i, chunkTag);
|
|
102
|
+
const cipher = (0, chacha_js_1.xchacha20poly1305)(cek, nonce);
|
|
103
|
+
const encrypted = cipher.encrypt(chunk);
|
|
104
|
+
output[offset] = chunkTag;
|
|
105
|
+
offset += 1;
|
|
106
|
+
output.set(encrypted, offset);
|
|
107
|
+
offset += encrypted.length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function writeHeaderPrefix(out, chunkSize, baseNonce, count) {
|
|
111
|
+
out.set(exports.MULTI_PQ_STREAM_MAGIC, 0);
|
|
112
|
+
out[4] = exports.MULTI_PQ_STREAM_VERSION;
|
|
113
|
+
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
114
|
+
view.setUint32(5, chunkSize, false);
|
|
115
|
+
out.set(baseNonce, 9);
|
|
116
|
+
view.setUint16(33, count, false);
|
|
117
|
+
}
|
|
118
|
+
function streamMultiPqEncrypt(options) {
|
|
119
|
+
const recipients = options.recipients;
|
|
120
|
+
if (!Array.isArray(recipients) || recipients.length === 0) {
|
|
121
|
+
throw new Error("At least one recipient is required");
|
|
122
|
+
}
|
|
123
|
+
if (recipients.length > 1000) {
|
|
124
|
+
throw new Error("Recipient count exceeds maximum limit of 1000");
|
|
125
|
+
}
|
|
126
|
+
const chunkSize = options.chunkSize ?? stream_pq_aead_1.DEFAULT_CHUNK_SIZE;
|
|
127
|
+
if (chunkSize < 1024 || chunkSize > 16 * 1024 * 1024) {
|
|
128
|
+
throw new Error("Chunk size must be between 1024 and 16777216 bytes");
|
|
129
|
+
}
|
|
130
|
+
const cek = (0, utils_js_1.randomBytes)(exports.CEK_LEN);
|
|
131
|
+
const baseNonce = (0, utils_js_1.randomBytes)(stream_pq_aead_1.NONCE_LEN);
|
|
132
|
+
const serializedSlots = [];
|
|
133
|
+
let slotsTotalLen = 0;
|
|
134
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
135
|
+
const slotBytes = createSlot(cek, i, recipients[i]);
|
|
136
|
+
serializedSlots.push(slotBytes);
|
|
137
|
+
slotsTotalLen += slotBytes.length;
|
|
138
|
+
}
|
|
139
|
+
const headerLen = exports.MULTI_PQ_PREFIX_LEN + slotsTotalLen;
|
|
140
|
+
const totalSize = computeMultiStreamOutputSize(headerLen, options.plaintext.length, chunkSize);
|
|
141
|
+
const output = new Uint8Array(totalSize);
|
|
142
|
+
writeHeaderPrefix(output, chunkSize, baseNonce, recipients.length);
|
|
143
|
+
let slotOffset = exports.MULTI_PQ_PREFIX_LEN;
|
|
144
|
+
for (const s of serializedSlots) {
|
|
145
|
+
output.set(s, slotOffset);
|
|
146
|
+
slotOffset += s.length;
|
|
147
|
+
}
|
|
148
|
+
encryptStreamChunks(options.plaintext, cek, baseNonce, chunkSize, output, headerLen);
|
|
149
|
+
(0, utils_1.wipeMemory)(cek);
|
|
150
|
+
return {
|
|
151
|
+
ciphertext: output,
|
|
152
|
+
algorithm: "multi-x25519-ml-kem-768-xchacha20-poly1305-stream",
|
|
153
|
+
recipientCount: recipients.length,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function parseHeaderPrefix(ciphertext) {
|
|
157
|
+
if (ciphertext.length < exports.MULTI_PQ_PREFIX_LEN) {
|
|
158
|
+
throw new Error("Ciphertext too short: missing header prefix");
|
|
159
|
+
}
|
|
160
|
+
for (let i = 0; i < 4; i++) {
|
|
161
|
+
if (ciphertext[i] !== exports.MULTI_PQ_STREAM_MAGIC[i]) {
|
|
162
|
+
throw new Error("Invalid multi-recipient post-quantum stream magic");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (ciphertext[4] !== exports.MULTI_PQ_STREAM_VERSION) {
|
|
166
|
+
throw new Error(`Unsupported multi-recipient post-quantum stream version: ${ciphertext[4]}`);
|
|
167
|
+
}
|
|
168
|
+
const view = new DataView(ciphertext.buffer, ciphertext.byteOffset, ciphertext.byteLength);
|
|
169
|
+
const chunkSize = view.getUint32(5, false);
|
|
170
|
+
if (chunkSize < 1024 || chunkSize > 16 * 1024 * 1024) {
|
|
171
|
+
throw new Error(`Invalid stream header chunk size: ${chunkSize}`);
|
|
172
|
+
}
|
|
173
|
+
const baseNonce = ciphertext.subarray(9, 33);
|
|
174
|
+
const recipientCount = view.getUint16(33, false);
|
|
175
|
+
if (recipientCount < 1) {
|
|
176
|
+
throw new Error("Invalid stream header: zero recipients");
|
|
177
|
+
}
|
|
178
|
+
return { chunkSize, baseNonce, recipientCount };
|
|
179
|
+
}
|
|
180
|
+
function parseSlots(ciphertext, recipientCount) {
|
|
181
|
+
let offset = exports.MULTI_PQ_PREFIX_LEN;
|
|
182
|
+
const slots = [];
|
|
183
|
+
for (let i = 0; i < recipientCount; i++) {
|
|
184
|
+
if (offset >= ciphertext.length) {
|
|
185
|
+
throw new Error("Ciphertext truncated: incomplete slot table");
|
|
186
|
+
}
|
|
187
|
+
const idLen = ciphertext[offset];
|
|
188
|
+
const slotTotalLen = exports.SLOT_FIXED_OVERHEAD + idLen;
|
|
189
|
+
if (offset + slotTotalLen > ciphertext.length) {
|
|
190
|
+
throw new Error("Ciphertext truncated: incomplete recipient slot");
|
|
191
|
+
}
|
|
192
|
+
const idStart = offset + 1;
|
|
193
|
+
const recipientIdBytes = ciphertext.subarray(idStart, idStart + idLen);
|
|
194
|
+
const recipientId = TEXT_DECODER.decode(recipientIdBytes);
|
|
195
|
+
let pos = idStart + idLen;
|
|
196
|
+
const ephPub = ciphertext.subarray(pos, pos + stream_pq_aead_1.EPHEMERAL_LEN);
|
|
197
|
+
pos += stream_pq_aead_1.EPHEMERAL_LEN;
|
|
198
|
+
const mlKemCt = ciphertext.subarray(pos, pos + stream_pq_aead_1.ML_KEM_CT_LEN);
|
|
199
|
+
pos += stream_pq_aead_1.ML_KEM_CT_LEN;
|
|
200
|
+
const slotNonce = ciphertext.subarray(pos, pos + stream_pq_aead_1.NONCE_LEN);
|
|
201
|
+
pos += stream_pq_aead_1.NONCE_LEN;
|
|
202
|
+
const wrappedCek = ciphertext.subarray(pos, pos + exports.WRAPPED_CEK_LEN);
|
|
203
|
+
slots.push({
|
|
204
|
+
recipientId,
|
|
205
|
+
recipientIdBytes,
|
|
206
|
+
slotIndex: i,
|
|
207
|
+
ephPub,
|
|
208
|
+
mlKemCt,
|
|
209
|
+
slotNonce,
|
|
210
|
+
wrappedCek,
|
|
211
|
+
});
|
|
212
|
+
offset += slotTotalLen;
|
|
213
|
+
}
|
|
214
|
+
return { slots, headerEndOffset: offset };
|
|
215
|
+
}
|
|
216
|
+
function tryUnwrapSlot(slot, recipX25519Sec, recipMlKemSec) {
|
|
217
|
+
try {
|
|
218
|
+
const mlKemShared = ml_kem_js_1.ml_kem768.decapsulate(slot.mlKemCt, recipMlKemSec);
|
|
219
|
+
const x25519Shared = ed25519_js_1.x25519.getSharedSecret(recipX25519Sec, slot.ephPub);
|
|
220
|
+
const combined = new Uint8Array(64);
|
|
221
|
+
combined.set(x25519Shared, 0);
|
|
222
|
+
combined.set(mlKemShared, 32);
|
|
223
|
+
const kek = (0, hkdf_js_1.hkdf)(sha2_js_1.sha256, combined, slot.ephPub, KEK_INFO, 32);
|
|
224
|
+
const aad = buildSlotAad(slot.recipientIdBytes, slot.slotIndex);
|
|
225
|
+
const cek = (0, chacha_js_1.xchacha20poly1305)(kek, slot.slotNonce, aad).decrypt(slot.wrappedCek);
|
|
226
|
+
(0, utils_1.wipeMemory)(x25519Shared);
|
|
227
|
+
(0, utils_1.wipeMemory)(mlKemShared);
|
|
228
|
+
(0, utils_1.wipeMemory)(combined);
|
|
229
|
+
(0, utils_1.wipeMemory)(kek);
|
|
230
|
+
return cek;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function readMultiChunkHeader(ciphertext, offset, chunkSize) {
|
|
237
|
+
const chunkTag = ciphertext[offset];
|
|
238
|
+
if (chunkTag !== stream_pq_aead_1.CHUNK_TAG_MESSAGE && chunkTag !== stream_pq_aead_1.CHUNK_TAG_FINAL) {
|
|
239
|
+
throw new Error(`Invalid chunk tag: 0x${chunkTag.toString(16)}`);
|
|
240
|
+
}
|
|
241
|
+
const isFinal = chunkTag === stream_pq_aead_1.CHUNK_TAG_FINAL;
|
|
242
|
+
const start = offset + 1;
|
|
243
|
+
const encLen = isFinal ? ciphertext.length - start : chunkSize + stream_pq_aead_1.TAG_LEN;
|
|
244
|
+
if (start + encLen > ciphertext.length) {
|
|
245
|
+
throw new Error("Ciphertext truncated: incomplete chunk");
|
|
246
|
+
}
|
|
247
|
+
return { chunkTag, isFinal, encLen };
|
|
248
|
+
}
|
|
249
|
+
function decryptStreamChunks(ciphertext, headerEndOffset, cek, baseNonce, chunkSize) {
|
|
250
|
+
let offset = headerEndOffset;
|
|
251
|
+
const chunks = [];
|
|
252
|
+
let totalPtLen = 0;
|
|
253
|
+
let sawFinal = false;
|
|
254
|
+
while (offset < ciphertext.length) {
|
|
255
|
+
const { chunkTag, isFinal, encLen } = readMultiChunkHeader(ciphertext, offset, chunkSize);
|
|
256
|
+
offset += 1;
|
|
257
|
+
const chunkIdx = chunks.length;
|
|
258
|
+
const nonce = (0, stream_pq_aead_1.derivePqChunkNonce)(baseNonce, chunkIdx, chunkTag);
|
|
259
|
+
const cipher = (0, chacha_js_1.xchacha20poly1305)(cek, nonce);
|
|
260
|
+
const decrypted = cipher.decrypt(ciphertext.subarray(offset, offset + encLen));
|
|
261
|
+
chunks.push(decrypted);
|
|
262
|
+
totalPtLen += decrypted.length;
|
|
263
|
+
offset += encLen;
|
|
264
|
+
if (isFinal) {
|
|
265
|
+
sawFinal = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!sawFinal) {
|
|
270
|
+
throw new Error("Ciphertext truncated: final chunk missing");
|
|
271
|
+
}
|
|
272
|
+
const result = new Uint8Array(totalPtLen);
|
|
273
|
+
let pos = 0;
|
|
274
|
+
for (const chunk of chunks) {
|
|
275
|
+
result.set(chunk, pos);
|
|
276
|
+
pos += chunk.length;
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
function resolveRecipientCek(slots, recipId, recipX25519Sec, recipMlKemSec) {
|
|
281
|
+
if (recipId !== undefined) {
|
|
282
|
+
const slot = slots.find((s) => s.recipientId === recipId) ?? null;
|
|
283
|
+
if (!slot) {
|
|
284
|
+
throw new Error(`Recipient ID "${recipId}" not found in stream slots`);
|
|
285
|
+
}
|
|
286
|
+
const cek = tryUnwrapSlot(slot, recipX25519Sec, recipMlKemSec);
|
|
287
|
+
if (!cek) {
|
|
288
|
+
throw new Error(`Decryption failed for recipient slot "${recipId}"`);
|
|
289
|
+
}
|
|
290
|
+
return { cek, matchingSlot: slot };
|
|
291
|
+
}
|
|
292
|
+
for (const slot of slots) {
|
|
293
|
+
const cek = tryUnwrapSlot(slot, recipX25519Sec, recipMlKemSec);
|
|
294
|
+
if (cek) {
|
|
295
|
+
return { cek, matchingSlot: slot };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw new Error("No matching recipient key slot could be decrypted");
|
|
299
|
+
}
|
|
300
|
+
function streamMultiPqDecrypt(options) {
|
|
301
|
+
const recipX25519Sec = (0, stream_pq_aead_1.toBytes)(options.recipientX25519Secret, 32, "recipientX25519Secret");
|
|
302
|
+
const recipMlKemSec = (0, stream_pq_aead_1.toBytes)(options.recipientMlKemSecret, 2400, "recipientMlKemSecret");
|
|
303
|
+
const ciphertext = options.ciphertext;
|
|
304
|
+
const { chunkSize: headerChunkSize, baseNonce, recipientCount, } = parseHeaderPrefix(ciphertext);
|
|
305
|
+
const effectiveChunkSize = options.chunkSize ?? headerChunkSize;
|
|
306
|
+
const { slots, headerEndOffset } = parseSlots(ciphertext, recipientCount);
|
|
307
|
+
const { cek, matchingSlot } = resolveRecipientCek(slots, options.recipientId, recipX25519Sec, recipMlKemSec);
|
|
308
|
+
const plaintext = decryptStreamChunks(ciphertext, headerEndOffset, cek, baseNonce, effectiveChunkSize);
|
|
309
|
+
(0, utils_1.wipeMemory)(cek);
|
|
310
|
+
return {
|
|
311
|
+
plaintext,
|
|
312
|
+
recipientId: matchingSlot.recipientId,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=stream-multi-pq-aead.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const EPHEMERAL_LEN = 32;
|
|
2
|
+
export declare const ML_KEM_CT_LEN = 1088;
|
|
3
|
+
export declare const NONCE_LEN = 24;
|
|
4
|
+
export declare const TAG_LEN = 16;
|
|
5
|
+
export declare const PQ_STREAM_HEADER_LEN: number;
|
|
6
|
+
export declare const DEFAULT_CHUNK_SIZE: number;
|
|
7
|
+
export declare const CHUNK_TAG_MESSAGE = 0;
|
|
8
|
+
export declare const CHUNK_TAG_FINAL = 1;
|
|
9
|
+
export declare function toBytes(data: string | Uint8Array, expectedLen?: number, name?: string): Uint8Array;
|
|
10
|
+
export declare function derivePqChunkNonce(baseNonce: Uint8Array, counter: number, tag: number): Uint8Array;
|
|
11
|
+
export interface StreamPqEncryptOptions {
|
|
12
|
+
recipientX25519Public: string | Uint8Array;
|
|
13
|
+
recipientMlKemPublic: string | Uint8Array;
|
|
14
|
+
plaintext: Uint8Array;
|
|
15
|
+
chunkSize?: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface StreamPqEncryptResult {
|
|
18
|
+
ciphertext: Uint8Array;
|
|
19
|
+
algorithm: "x25519-ml-kem-768-xchacha20-poly1305-stream";
|
|
20
|
+
}
|
|
21
|
+
export interface StreamPqDecryptOptions {
|
|
22
|
+
recipientX25519Secret: string | Uint8Array;
|
|
23
|
+
recipientMlKemSecret: string | Uint8Array;
|
|
24
|
+
ciphertext: Uint8Array;
|
|
25
|
+
chunkSize?: number | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare function streamPqEncrypt(options: StreamPqEncryptOptions): StreamPqEncryptResult;
|
|
28
|
+
export declare function streamPqDecrypt(options: StreamPqDecryptOptions): Uint8Array;
|
|
29
|
+
//# sourceMappingURL=stream-pq-aead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-pq-aead.d.ts","sourceRoot":"","sources":["../../src/streaming/stream-pq-aead.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,eAAO,MAAM,OAAO,KAAK,CAAC;AAE1B,eAAO,MAAM,oBAAoB,QAA4C,CAAC;AAE9E,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,eAAO,MAAM,iBAAiB,IAAO,CAAC;AAEtC,eAAO,MAAM,eAAe,IAAO,CAAC;AAKpC,wBAAgB,OAAO,CACrB,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,SAAS,GACZ,UAAU,CAYZ;AAGD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,UAAU,CAiBZ;AAGD,MAAM,WAAW,sBAAsB;IACrC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1C,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,6CAA6C,CAAC;CAC1D;AAGD,MAAM,WAAW,sBAAsB;IACrC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1C,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAqGD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAiCvB;AA6ED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,UAAU,CAoC3E"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CHUNK_TAG_FINAL = exports.CHUNK_TAG_MESSAGE = exports.DEFAULT_CHUNK_SIZE = exports.PQ_STREAM_HEADER_LEN = exports.TAG_LEN = exports.NONCE_LEN = exports.ML_KEM_CT_LEN = exports.EPHEMERAL_LEN = void 0;
|
|
4
|
+
exports.toBytes = toBytes;
|
|
5
|
+
exports.derivePqChunkNonce = derivePqChunkNonce;
|
|
6
|
+
exports.streamPqEncrypt = streamPqEncrypt;
|
|
7
|
+
exports.streamPqDecrypt = streamPqDecrypt;
|
|
8
|
+
const ed25519_js_1 = require("@noble/curves/ed25519.js");
|
|
9
|
+
const ml_kem_js_1 = require("@noble/post-quantum/ml-kem.js");
|
|
10
|
+
const hkdf_js_1 = require("@noble/hashes/hkdf.js");
|
|
11
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
12
|
+
const chacha_js_1 = require("@noble/ciphers/chacha.js");
|
|
13
|
+
const utils_js_1 = require("@noble/ciphers/utils.js");
|
|
14
|
+
exports.EPHEMERAL_LEN = 32;
|
|
15
|
+
exports.ML_KEM_CT_LEN = 1088;
|
|
16
|
+
exports.NONCE_LEN = 24;
|
|
17
|
+
exports.TAG_LEN = 16;
|
|
18
|
+
exports.PQ_STREAM_HEADER_LEN = exports.EPHEMERAL_LEN + exports.ML_KEM_CT_LEN + exports.NONCE_LEN;
|
|
19
|
+
exports.DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
20
|
+
exports.CHUNK_TAG_MESSAGE = 0x00;
|
|
21
|
+
exports.CHUNK_TAG_FINAL = 0x01;
|
|
22
|
+
const HEX_RE = /^[0-9a-fA-F]*$/;
|
|
23
|
+
function toBytes(data, expectedLen, name = "Data") {
|
|
24
|
+
let buf;
|
|
25
|
+
if (typeof data === "string") {
|
|
26
|
+
if (!HEX_RE.test(data))
|
|
27
|
+
throw new Error(`Invalid hex string for ${name}`);
|
|
28
|
+
buf = Buffer.from(data, "hex");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
buf = new Uint8Array(data);
|
|
32
|
+
}
|
|
33
|
+
if (expectedLen !== undefined && buf.length !== expectedLen) {
|
|
34
|
+
throw new Error(`${name} must be ${expectedLen} bytes, got ${buf.length}`);
|
|
35
|
+
}
|
|
36
|
+
return buf;
|
|
37
|
+
}
|
|
38
|
+
function derivePqChunkNonce(baseNonce, counter, tag) {
|
|
39
|
+
const nonce = new Uint8Array(baseNonce);
|
|
40
|
+
const view = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength);
|
|
41
|
+
const low = counter & 0xffffffff;
|
|
42
|
+
const high = (counter / 0x100000000) | 0;
|
|
43
|
+
view.setUint32(nonce.length - 8, view.getUint32(nonce.length - 8) ^ low, true);
|
|
44
|
+
view.setUint32(nonce.length - 4, view.getUint32(nonce.length - 4) ^ high, true);
|
|
45
|
+
nonce[0] = nonce[0] ^ tag;
|
|
46
|
+
return nonce;
|
|
47
|
+
}
|
|
48
|
+
function deriveSharedKey(x25519Shared, mlKemShared, ephPub) {
|
|
49
|
+
const combined = new Uint8Array(x25519Shared.length + mlKemShared.length);
|
|
50
|
+
combined.set(x25519Shared);
|
|
51
|
+
combined.set(mlKemShared, x25519Shared.length);
|
|
52
|
+
return (0, hkdf_js_1.hkdf)(sha2_js_1.sha256, combined, ephPub, new TextEncoder().encode("pq-stream-aead-v1"), 32);
|
|
53
|
+
}
|
|
54
|
+
function encapsulateKeyMaterial(recipX25519Pub, recipMlKemPub) {
|
|
55
|
+
const ephPriv = (0, utils_js_1.randomBytes)(32);
|
|
56
|
+
const ephPub = ed25519_js_1.x25519.getPublicKey(ephPriv);
|
|
57
|
+
const x25519Shared = ed25519_js_1.x25519.getSharedSecret(ephPriv, recipX25519Pub);
|
|
58
|
+
const { cipherText: mlKemCt, sharedSecret: mlKemShared } = ml_kem_js_1.ml_kem768.encapsulate(recipMlKemPub);
|
|
59
|
+
const key = deriveSharedKey(x25519Shared, mlKemShared, ephPub);
|
|
60
|
+
return { ephPub, mlKemCt, key };
|
|
61
|
+
}
|
|
62
|
+
function decapsulateKeyMaterial(recipX25519Sec, recipMlKemSec, ephPub, mlKemCt) {
|
|
63
|
+
const mlKemShared = ml_kem_js_1.ml_kem768.decapsulate(mlKemCt, recipMlKemSec);
|
|
64
|
+
const x25519Shared = ed25519_js_1.x25519.getSharedSecret(recipX25519Sec, ephPub);
|
|
65
|
+
return deriveSharedKey(x25519Shared, mlKemShared, ephPub);
|
|
66
|
+
}
|
|
67
|
+
function computeStreamOutputSize(ptLen, chunkSize) {
|
|
68
|
+
const numFullChunks = Math.floor(ptLen / chunkSize);
|
|
69
|
+
const lastChunkLen = ptLen - numFullChunks * chunkSize;
|
|
70
|
+
const hasExtraChunk = lastChunkLen > 0 || ptLen === 0;
|
|
71
|
+
const totalChunks = hasExtraChunk ? numFullChunks + 1 : numFullChunks;
|
|
72
|
+
let totalSize = exports.PQ_STREAM_HEADER_LEN;
|
|
73
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
74
|
+
const len = i < numFullChunks ? chunkSize : lastChunkLen > 0 ? lastChunkLen : 0;
|
|
75
|
+
totalSize += 1 + len + exports.TAG_LEN;
|
|
76
|
+
}
|
|
77
|
+
return totalSize;
|
|
78
|
+
}
|
|
79
|
+
function encryptChunks(plaintext, key, baseNonce, chunkSize, output) {
|
|
80
|
+
const numFullChunks = Math.floor(plaintext.length / chunkSize);
|
|
81
|
+
const hasExtra = plaintext.length % chunkSize > 0 || plaintext.length === 0;
|
|
82
|
+
const totalChunks = hasExtra ? numFullChunks + 1 : numFullChunks;
|
|
83
|
+
let offset = exports.PQ_STREAM_HEADER_LEN;
|
|
84
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
85
|
+
const isFinal = i === totalChunks - 1;
|
|
86
|
+
const chunkTag = isFinal ? exports.CHUNK_TAG_FINAL : exports.CHUNK_TAG_MESSAGE;
|
|
87
|
+
const start = i * chunkSize;
|
|
88
|
+
const end = isFinal ? plaintext.length : start + chunkSize;
|
|
89
|
+
const chunk = plaintext.subarray(start, end);
|
|
90
|
+
const nonce = derivePqChunkNonce(baseNonce, i, chunkTag);
|
|
91
|
+
const cipher = (0, chacha_js_1.xchacha20poly1305)(key, nonce);
|
|
92
|
+
const encrypted = cipher.encrypt(chunk);
|
|
93
|
+
output[offset] = chunkTag;
|
|
94
|
+
offset += 1;
|
|
95
|
+
output.set(encrypted, offset);
|
|
96
|
+
offset += encrypted.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function streamPqEncrypt(options) {
|
|
100
|
+
const recipX25519Pub = toBytes(options.recipientX25519Public, 32, "recipientX25519Public");
|
|
101
|
+
const recipMlKemPub = toBytes(options.recipientMlKemPublic, 1184, "recipientMlKemPublic");
|
|
102
|
+
const chunkSize = options.chunkSize ?? exports.DEFAULT_CHUNK_SIZE;
|
|
103
|
+
const plaintext = options.plaintext;
|
|
104
|
+
const { ephPub, mlKemCt, key } = encapsulateKeyMaterial(recipX25519Pub, recipMlKemPub);
|
|
105
|
+
const baseNonce = (0, utils_js_1.randomBytes)(exports.NONCE_LEN);
|
|
106
|
+
const totalSize = computeStreamOutputSize(plaintext.length, chunkSize);
|
|
107
|
+
const output = new Uint8Array(totalSize);
|
|
108
|
+
output.set(ephPub, 0);
|
|
109
|
+
output.set(mlKemCt, exports.EPHEMERAL_LEN);
|
|
110
|
+
output.set(baseNonce, exports.EPHEMERAL_LEN + exports.ML_KEM_CT_LEN);
|
|
111
|
+
encryptChunks(plaintext, key, baseNonce, chunkSize, output);
|
|
112
|
+
return {
|
|
113
|
+
ciphertext: output,
|
|
114
|
+
algorithm: "x25519-ml-kem-768-xchacha20-poly1305-stream",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function readPqChunkHeader(ciphertext, offset, chunkSize) {
|
|
118
|
+
const chunkTag = ciphertext[offset];
|
|
119
|
+
if (chunkTag !== exports.CHUNK_TAG_MESSAGE && chunkTag !== exports.CHUNK_TAG_FINAL) {
|
|
120
|
+
throw new Error(`Invalid chunk tag: 0x${chunkTag.toString(16)}`);
|
|
121
|
+
}
|
|
122
|
+
const isFinal = chunkTag === exports.CHUNK_TAG_FINAL;
|
|
123
|
+
const start = offset + 1;
|
|
124
|
+
const encLen = isFinal ? ciphertext.length - start : chunkSize + exports.TAG_LEN;
|
|
125
|
+
if (start + encLen > ciphertext.length) {
|
|
126
|
+
throw new Error("Ciphertext truncated — incomplete chunk");
|
|
127
|
+
}
|
|
128
|
+
return { chunkTag, isFinal, encLen };
|
|
129
|
+
}
|
|
130
|
+
function decryptChunks(ciphertext, key, baseNonce, chunkSize) {
|
|
131
|
+
let offset = exports.PQ_STREAM_HEADER_LEN;
|
|
132
|
+
const chunks = [];
|
|
133
|
+
let totalPtLen = 0;
|
|
134
|
+
let sawFinal = false;
|
|
135
|
+
while (offset < ciphertext.length) {
|
|
136
|
+
const { chunkTag, isFinal, encLen } = readPqChunkHeader(ciphertext, offset, chunkSize);
|
|
137
|
+
offset += 1;
|
|
138
|
+
const chunkIdx = chunks.length;
|
|
139
|
+
const nonce = derivePqChunkNonce(baseNonce, chunkIdx, chunkTag);
|
|
140
|
+
const cipher = (0, chacha_js_1.xchacha20poly1305)(key, nonce);
|
|
141
|
+
const decrypted = cipher.decrypt(ciphertext.subarray(offset, offset + encLen));
|
|
142
|
+
chunks.push(decrypted);
|
|
143
|
+
totalPtLen += decrypted.length;
|
|
144
|
+
offset += encLen;
|
|
145
|
+
if (isFinal) {
|
|
146
|
+
sawFinal = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!sawFinal) {
|
|
151
|
+
throw new Error("Ciphertext truncated — final chunk missing");
|
|
152
|
+
}
|
|
153
|
+
const result = new Uint8Array(totalPtLen);
|
|
154
|
+
let pos = 0;
|
|
155
|
+
for (const chunk of chunks) {
|
|
156
|
+
result.set(chunk, pos);
|
|
157
|
+
pos += chunk.length;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function streamPqDecrypt(options) {
|
|
162
|
+
const recipX25519Sec = toBytes(options.recipientX25519Secret, 32, "recipientX25519Secret");
|
|
163
|
+
const recipMlKemSec = toBytes(options.recipientMlKemSecret, 2400, "recipientMlKemSecret");
|
|
164
|
+
const chunkSize = options.chunkSize ?? exports.DEFAULT_CHUNK_SIZE;
|
|
165
|
+
const ciphertext = options.ciphertext;
|
|
166
|
+
if (ciphertext.length < exports.PQ_STREAM_HEADER_LEN + 1 + exports.TAG_LEN) {
|
|
167
|
+
throw new Error("Ciphertext too short — missing header or chunk data");
|
|
168
|
+
}
|
|
169
|
+
const ephPub = ciphertext.subarray(0, exports.EPHEMERAL_LEN);
|
|
170
|
+
const mlKemCt = ciphertext.subarray(exports.EPHEMERAL_LEN, exports.EPHEMERAL_LEN + exports.ML_KEM_CT_LEN);
|
|
171
|
+
const baseNonce = ciphertext.subarray(exports.EPHEMERAL_LEN + exports.ML_KEM_CT_LEN, exports.PQ_STREAM_HEADER_LEN);
|
|
172
|
+
const key = decapsulateKeyMaterial(recipX25519Sec, recipMlKemSec, ephPub, mlKemCt);
|
|
173
|
+
return decryptChunks(ciphertext, key, baseNonce, chunkSize);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=stream-pq-aead.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MultiPqRecipient } from "./stream-multi-pq-aead";
|
|
1
2
|
export type CryptoTransformStream<I, O> = import("stream/web").TransformStream<I, O>;
|
|
2
3
|
export type WebStreamHashAlgorithm = "sha256" | "sha512" | "sha3-256" | "sha3-512" | "blake3";
|
|
3
4
|
export declare const WEB_STREAM_HASH_ALGORITHMS: readonly WebStreamHashAlgorithm[];
|
|
@@ -14,4 +15,28 @@ export interface DecryptStreamOptions {
|
|
|
14
15
|
export declare function createEncryptStream(options: EncryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
15
16
|
export declare function createDecryptStream(options: DecryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
16
17
|
export declare function createHashStream(algorithm: WebStreamHashAlgorithm): CryptoTransformStream<Uint8Array, HashStreamResult>;
|
|
18
|
+
export interface PqEncryptStreamOptions {
|
|
19
|
+
recipientX25519Public: string | Uint8Array;
|
|
20
|
+
recipientMlKemPublic: string | Uint8Array;
|
|
21
|
+
chunkSize?: number | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface PqDecryptStreamOptions {
|
|
24
|
+
recipientX25519Secret: string | Uint8Array;
|
|
25
|
+
recipientMlKemSecret: string | Uint8Array;
|
|
26
|
+
chunkSize?: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface MultiPqEncryptStreamOptions {
|
|
29
|
+
recipients: MultiPqRecipient[];
|
|
30
|
+
chunkSize?: number | undefined;
|
|
31
|
+
}
|
|
32
|
+
export interface MultiPqDecryptStreamOptions {
|
|
33
|
+
recipientId?: string | undefined;
|
|
34
|
+
recipientX25519Secret: string | Uint8Array;
|
|
35
|
+
recipientMlKemSecret: string | Uint8Array;
|
|
36
|
+
chunkSize?: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
export declare function createPqEncryptStream(options: PqEncryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
39
|
+
export declare function createPqDecryptStream(options: PqDecryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
40
|
+
export declare function createMultiPqEncryptStream(options: MultiPqEncryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
41
|
+
export declare function createMultiPqDecryptStream(options: MultiPqDecryptStreamOptions): CryptoTransformStream<Uint8Array, Uint8Array>;
|
|
17
42
|
//# sourceMappingURL=web-streams.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-streams.d.ts","sourceRoot":"","sources":["../../src/streaming/web-streams.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"web-streams.d.ts","sourceRoot":"","sources":["../../src/streaming/web-streams.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAQhC,MAAM,MAAM,qBAAqB,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,YAAY,EAAE,eAAe,CAC5E,CAAC,EACD,CAAC,CACF,CAAC;AAMF,MAAM,MAAM,sBAAsB,GAChC,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAY3D,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAM9D,CAAC;AAUX,MAAM,WAAW,gBAAgB;IAE/B,MAAM,EAAE,MAAM,CAAC;IAEf,SAAS,EAAE,sBAAsB,CAAC;CACnC;AAUD,MAAM,WAAW,oBAAoB;IAEnC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CAC1B;AAUD,MAAM,WAAW,oBAAoB;IAEnC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CAC1B;AAkDD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CA8B/C;AAoBD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CA8B/C;AAuBD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,sBAAsB,GAChC,qBAAqB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAgBrD;AAGD,MAAM,WAAW,sBAAsB;IAErC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE1C,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,sBAAsB;IAErC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE1C,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,2BAA2B;IAE1C,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAE/B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAGD,MAAM,WAAW,2BAA2B;IAE1C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjC,qBAAqB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE3C,oBAAoB,EAAE,MAAM,GAAG,UAAU,CAAC;IAE1C,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAUD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CAyB/C;AAUD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CAyB/C;AAUD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CAwB/C;AAUD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CA0B/C"}
|