@atlasauth/pca-mpc 0.1.0 → 0.3.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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Hybrid base OT — a SOUND combiner of the malicious-secure EC base OT (`ot.ts`/`ec.ts`) and the
3
+ * post-quantum committed ML-KEM base OT (`kem-ot.ts`), run together so the stack inherits the BEST
4
+ * guarantee of each half instead of the weakest.
5
+ *
6
+ * ─── WHY A COMBINER ────────────────────────────────────────────────────────────────────────────────
7
+ * The two base OTs this package ships have complementary weaknesses:
8
+ * • The EC base OT (`malBaseRandomOT`) is MALICIOUS-secure against both roles — a Schnorr NIZK proof
9
+ * of knowledge of `y` on `S = y·B`, full prime-order-subgroup validation of every transmitted
10
+ * point, and a random-OT core with no sender-chosen ciphertexts — but its hardness is the curve25519
11
+ * discrete log, which a QUANTUM adversary breaks with Shor.
12
+ * • The ML-KEM base OT (`kemBaseOtMalRandom`) is POST-QUANTUM (Module-LWE) and has unconditional,
13
+ * even-against-a-quantum-sender choice privacy, but against an actively-malicious receiver it has an
14
+ * IRREDUCIBLE residue from the opaque primitive: a receiver that runs keygen twice and keeps BOTH
15
+ * secret keys learns both messages, and the decisional-MLWE indistinguishability that gives choice
16
+ * privacy makes that cheat undetectable to the sender (see `kem-ot.ts` header).
17
+ *
18
+ * The combiner runs BOTH base OTs on the SAME choice bit and the SAME session, then derives each
19
+ * transfer key from BOTH halves' shared secrets:
20
+ *
21
+ * k_b = H( DOMAIN_SEP ‖ sid ‖ i ‖ b ‖ ss_ec_b ‖ ss_kem_b ) for b ∈ {0, 1}
22
+ *
23
+ * where `ss_ec_b` is the EC random-OT pad (`malBaseRandomOT`) and `ss_kem_b` the committed ML-KEM
24
+ * random-OT secret (`kemBaseOtMalRandom`, the hardened variant — never the semi-honest one). The sender
25
+ * one-time-pads `m_b` under `k_b` and commits to it (`com_b`); the honest receiver reconstructs only
26
+ * `k_c` (it holds only `ss_ec_c` and `ss_kem_c`) and checks `com_c`.
27
+ *
28
+ * ─── WHAT THIS ACHIEVES (precise) ───────────────────────────────────────────────────────────────────
29
+ * To recover `m_{1−c}` an adversary needs `k_{1−c}`, hence BOTH `ss_ec_{1−c}` AND `ss_kem_{1−c}` (the
30
+ * hash is a random oracle; missing either input leaves the pad pseudorandom). Therefore:
31
+ * • vs a CLASSICAL malicious receiver — MALICIOUS-secure. Even a receiver that defeats the KEM half
32
+ * entirely by keeping both ML-KEM secrets (so it holds `ss_kem_{1−c}`) still cannot obtain
33
+ * `ss_ec_{1−c}`: the EC base OT is malicious-secure and `ss_ec_{1−c} = H(y·R − T)` is a point the
34
+ * receiver cannot compute without the discrete log of `S`. No `ss_ec_{1−c}` ⇒ no `k_{1−c}` ⇒ `m_{1−c}`
35
+ * stays hidden. This is the KILLER property the tests prove.
36
+ * • SENDER PRIVACY vs a SEMI-HONEST QUANTUM receiver — holds via the KEM half (Module-LWE): a quantum
37
+ * receiver that follows the protocol (discards the non-chosen material) cannot get `ss_kem_{1−c}`,
38
+ * so again `k_{1−c}` is unobtainable even though it could break the EC half with Shor.
39
+ * • CHOICE PRIVACY — POST-QUANTUM. The KEM half's choice privacy is unconditional (two i.i.d. real
40
+ * ML-KEM public keys on the wire); the EC half's is information-theoretic in its transcript (`R` is a
41
+ * uniform subgroup element whatever `c` is). Neither leaks `c`, against any adversary.
42
+ * • SESSION BINDING — both halves are bound to a COMMON `sid` derived from BOTH sub-protocols'
43
+ * session material (the KEM `(rn, sn)` nonces and the EC point `S`), and `sid` is hashed into every
44
+ * transfer key and commitment, so secrets from one session/opened-key-set cannot be replayed into
45
+ * another. The KEM half additionally aborts (`KemBaseOtAbort`) if the receiver opens keys other than
46
+ * the ones it committed to; the EC half aborts (`BaseOtAbort`/`PointValidationError`) on a bad proof
47
+ * or an off-/small-order point; and the combiner aborts (`HybridOtAbort`) if a tampered ciphertext
48
+ * fails its commitment.
49
+ *
50
+ * ─── THE HONEST BOUNDARY (NOT faked) ────────────────────────────────────────────────────────────────
51
+ * The one residue this combiner does NOT close is a receiver that is SIMULTANEOUSLY actively-malicious
52
+ * AND quantum: such a receiver could break the classical EC half with Shor (recovering `ss_ec_{1−c}`
53
+ * from the transcript) WHILE keeping both ML-KEM secrets (recovering `ss_kem_{1−c}`), and so assemble
54
+ * `k_{1−c}`. Closing THAT requires a base OT that is BOTH post-quantum AND malicious-secure in one
55
+ * primitive — i.e. an endemic/UC OT from Module-LWE with a CRS, which needs direct lattice module
56
+ * arithmetic (sampling / adding in ML-KEM's structured public-key space to plant a secretless slot).
57
+ * The opaque `ml_kem768` keygen/encaps/decaps interface this package is restricted to does NOT expose
58
+ * that arithmetic, and hand-rolling it is exactly the lattice crypto the module forbids. So this is the
59
+ * honest limit of what a KEM-opaque + EC combiner can reach; it is stated, not papered over.
60
+ *
61
+ * No hand-rolled crypto: the EC half goes through `malBaseRandomOT` (unchanged), the KEM half through
62
+ * `kemBaseOtMalRandom` (unchanged), and this module only hashes (SHA-256 RO) and XORs their outputs.
63
+ */
64
+ import { type Point } from './ec';
65
+ import { type BaseOtTamper, type OtChannel, type OtRandom } from './ot';
66
+ import { type KemMalTamper } from './kem-ot';
67
+ /** Raised when the hybrid combiner aborts: a sender ciphertext inconsistent with its commitment is caught. */
68
+ export declare class HybridOtAbort extends Error {
69
+ constructor(message: string);
70
+ }
71
+ /**
72
+ * The COMMON session id binding both halves: `H(TAG ‖ kemSid ‖ encode(ecS))`. It folds the KEM
73
+ * sub-protocol's two-nonce session id (`rn, sn`) and the EC sub-protocol's point `S`, so neither party
74
+ * alone fixes it and secrets cannot be mixed across sessions. Both parties derive the same value from
75
+ * the public transcript.
76
+ */
77
+ export declare function hybridSessionId(kemSid: Uint8Array, ecS: Point): Uint8Array;
78
+ /**
79
+ * The hybrid transfer key for one slot: `H(TAG ‖ sid ‖ i ‖ slot ‖ ss_ec ‖ ss_kem)` expanded (SHA-256
80
+ * counter mode) to `len` bytes. Modelled as a random oracle: the key is pseudorandom unless BOTH
81
+ * `ss_ec` and `ss_kem` (for that exact slot/index/session) are supplied — the soundness crux of the
82
+ * combiner.
83
+ */
84
+ export declare function hybridTransferKey(sid: Uint8Array, index: number, slot: number, ssEc: Uint8Array, ssKem: Uint8Array, len: number): Uint8Array;
85
+ /** The sender's wire message of the hybrid combiner: one-time-padded messages + per-message commitments. */
86
+ export interface HybridSenderMessage {
87
+ /** For each transfer i, `[E_0, E_1]` with `E_b = m_b ⊕ k_b`. */
88
+ encrypted: Array<[Uint8Array, Uint8Array]>;
89
+ /** For each transfer i, `[com_0, com_1]` with `com_b = H(sid ‖ i ‖ b ‖ m_b ‖ k_b)`. */
90
+ commitments: Array<[Uint8Array, Uint8Array]>;
91
+ }
92
+ /**
93
+ * SENDER side of the combiner: given BOTH halves' per-slot shared secrets, derive both transfer keys,
94
+ * one-time-pad each message, and commit to each. `ecSecrets[i] = [ss_ec_0, ss_ec_1]` (the EC random-OT
95
+ * `senderPads`), `kemSecrets[i] = [ss_kem_0, ss_kem_1]` (the committed-KEM `senderSecrets`).
96
+ */
97
+ export declare function hybridSenderEncrypt(sid: Uint8Array, messages: Array<[Uint8Array, Uint8Array]>, ecSecrets: Array<[Uint8Array, Uint8Array]>, kemSecrets: Array<[Uint8Array, Uint8Array]>): HybridSenderMessage;
98
+ /**
99
+ * RECEIVER side of the combiner: reconstruct ONLY the chosen transfer key (from the chosen EC and KEM
100
+ * secrets the receiver legitimately holds), unmask the chosen message, and CHECK the commitment.
101
+ * `ecRecv[i] = ss_ec_{choices[i]}` (the EC `recvPads`), `kemRecv[i] = ss_kem_{choices[i]}` (the
102
+ * committed-KEM `recvSecrets`). Aborts (`HybridOtAbort`) if the recovered message fails its commitment.
103
+ */
104
+ export declare function hybridReceiverRecover(sid: Uint8Array, choices: number[], sender: HybridSenderMessage, ecRecv: Uint8Array[], kemRecv: Uint8Array[]): Uint8Array[];
105
+ /**
106
+ * TEST-ONLY injection points modelling malicious behaviour in the hybrid combiner. The `ec` / `kem`
107
+ * bags are forwarded to the respective sub-OTs (so each half's real malicious checks run against the
108
+ * adversarial value); `tamperEncrypted` corrupts the hybrid sender's ciphertexts on the wire (caught by
109
+ * the commitment check).
110
+ */
111
+ export interface HybridBaseOtTamper {
112
+ /** Forwarded to the EC half (`malBaseRandomOT`): malicious sender/receiver on the EC sub-OT. */
113
+ ec?: BaseOtTamper;
114
+ /** Forwarded to the KEM half (`kemBaseOtMalRandom`): malicious receiver on the committed KEM sub-OT. */
115
+ kem?: KemMalTamper;
116
+ /** Malicious hybrid SENDER: corrupt the padded messages on the wire (inconsistent with the commitment). */
117
+ tamperEncrypted?: (encrypted: Array<[Uint8Array, Uint8Array]>) => void;
118
+ }
119
+ /** A full hybrid base-OT transcript, exposed for inspection/tests. */
120
+ export interface HybridBaseOtTranscript {
121
+ /** The common session id both halves are bound to. */
122
+ sid: Uint8Array;
123
+ /** The EC sub-OT's public point `S` (with its Schnorr proof available via the EC half). */
124
+ ecS: Point;
125
+ /** The committed-KEM sub-OT session id `H(rn ‖ sn)`. */
126
+ kemSid: Uint8Array;
127
+ encrypted: Array<[Uint8Array, Uint8Array]>;
128
+ commitments: Array<[Uint8Array, Uint8Array]>;
129
+ }
130
+ export interface HybridBaseOtResult {
131
+ /** Receiver's recovered messages `m_{i, choices[i]}`. */
132
+ received: Uint8Array[];
133
+ transcript: HybridBaseOtTranscript;
134
+ }
135
+ /**
136
+ * In-process combined simulator (both parties in one call), matching the shape of `baseOT`/`kemBaseOT`
137
+ * so it is a drop-in for `OtChannel`'s `baseOtFn` (`withHybridBaseOt`). Runs the malicious-secure EC
138
+ * base OT and the committed ML-KEM base OT on the SAME choices, binds both to a common `sid`, and
139
+ * combines their per-slot secrets into the transfer keys. A cheating party is caught by whichever
140
+ * half's check applies (EC proof/point validation, KEM commitment, or the hybrid ciphertext
141
+ * commitment). `tamper` injects malicious behaviour for tests.
142
+ */
143
+ export declare function hybridBaseOT(messages: Array<[Uint8Array, Uint8Array]>, choices: number[], rng?: OtRandom, tamper?: HybridBaseOtTamper): HybridBaseOtResult;
144
+ /**
145
+ * Wire the HYBRID base OT into the KOS OT-extension: a drop-in for `withKemBaseOt`/`withKemBaseOtMal`
146
+ * that makes `OtChannel` run its κ setup base OTs over the combiner. The extension inherits the
147
+ * combiner's guarantee — malicious security vs a classical receiver, PQ sender-privacy vs a semi-honest
148
+ * quantum one, and PQ choice privacy — with everything above the base OT (IKNP matrix, KOS correlation
149
+ * check, correlation-robust hash) unchanged.
150
+ *
151
+ * const ch = new OtChannel(secureOtRandom(), withHybridBaseOt());
152
+ */
153
+ export declare function withHybridBaseOt(): ConstructorParameters<typeof OtChannel>[1];
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ /**
3
+ * Hybrid base OT — a SOUND combiner of the malicious-secure EC base OT (`ot.ts`/`ec.ts`) and the
4
+ * post-quantum committed ML-KEM base OT (`kem-ot.ts`), run together so the stack inherits the BEST
5
+ * guarantee of each half instead of the weakest.
6
+ *
7
+ * ─── WHY A COMBINER ────────────────────────────────────────────────────────────────────────────────
8
+ * The two base OTs this package ships have complementary weaknesses:
9
+ * • The EC base OT (`malBaseRandomOT`) is MALICIOUS-secure against both roles — a Schnorr NIZK proof
10
+ * of knowledge of `y` on `S = y·B`, full prime-order-subgroup validation of every transmitted
11
+ * point, and a random-OT core with no sender-chosen ciphertexts — but its hardness is the curve25519
12
+ * discrete log, which a QUANTUM adversary breaks with Shor.
13
+ * • The ML-KEM base OT (`kemBaseOtMalRandom`) is POST-QUANTUM (Module-LWE) and has unconditional,
14
+ * even-against-a-quantum-sender choice privacy, but against an actively-malicious receiver it has an
15
+ * IRREDUCIBLE residue from the opaque primitive: a receiver that runs keygen twice and keeps BOTH
16
+ * secret keys learns both messages, and the decisional-MLWE indistinguishability that gives choice
17
+ * privacy makes that cheat undetectable to the sender (see `kem-ot.ts` header).
18
+ *
19
+ * The combiner runs BOTH base OTs on the SAME choice bit and the SAME session, then derives each
20
+ * transfer key from BOTH halves' shared secrets:
21
+ *
22
+ * k_b = H( DOMAIN_SEP ‖ sid ‖ i ‖ b ‖ ss_ec_b ‖ ss_kem_b ) for b ∈ {0, 1}
23
+ *
24
+ * where `ss_ec_b` is the EC random-OT pad (`malBaseRandomOT`) and `ss_kem_b` the committed ML-KEM
25
+ * random-OT secret (`kemBaseOtMalRandom`, the hardened variant — never the semi-honest one). The sender
26
+ * one-time-pads `m_b` under `k_b` and commits to it (`com_b`); the honest receiver reconstructs only
27
+ * `k_c` (it holds only `ss_ec_c` and `ss_kem_c`) and checks `com_c`.
28
+ *
29
+ * ─── WHAT THIS ACHIEVES (precise) ───────────────────────────────────────────────────────────────────
30
+ * To recover `m_{1−c}` an adversary needs `k_{1−c}`, hence BOTH `ss_ec_{1−c}` AND `ss_kem_{1−c}` (the
31
+ * hash is a random oracle; missing either input leaves the pad pseudorandom). Therefore:
32
+ * • vs a CLASSICAL malicious receiver — MALICIOUS-secure. Even a receiver that defeats the KEM half
33
+ * entirely by keeping both ML-KEM secrets (so it holds `ss_kem_{1−c}`) still cannot obtain
34
+ * `ss_ec_{1−c}`: the EC base OT is malicious-secure and `ss_ec_{1−c} = H(y·R − T)` is a point the
35
+ * receiver cannot compute without the discrete log of `S`. No `ss_ec_{1−c}` ⇒ no `k_{1−c}` ⇒ `m_{1−c}`
36
+ * stays hidden. This is the KILLER property the tests prove.
37
+ * • SENDER PRIVACY vs a SEMI-HONEST QUANTUM receiver — holds via the KEM half (Module-LWE): a quantum
38
+ * receiver that follows the protocol (discards the non-chosen material) cannot get `ss_kem_{1−c}`,
39
+ * so again `k_{1−c}` is unobtainable even though it could break the EC half with Shor.
40
+ * • CHOICE PRIVACY — POST-QUANTUM. The KEM half's choice privacy is unconditional (two i.i.d. real
41
+ * ML-KEM public keys on the wire); the EC half's is information-theoretic in its transcript (`R` is a
42
+ * uniform subgroup element whatever `c` is). Neither leaks `c`, against any adversary.
43
+ * • SESSION BINDING — both halves are bound to a COMMON `sid` derived from BOTH sub-protocols'
44
+ * session material (the KEM `(rn, sn)` nonces and the EC point `S`), and `sid` is hashed into every
45
+ * transfer key and commitment, so secrets from one session/opened-key-set cannot be replayed into
46
+ * another. The KEM half additionally aborts (`KemBaseOtAbort`) if the receiver opens keys other than
47
+ * the ones it committed to; the EC half aborts (`BaseOtAbort`/`PointValidationError`) on a bad proof
48
+ * or an off-/small-order point; and the combiner aborts (`HybridOtAbort`) if a tampered ciphertext
49
+ * fails its commitment.
50
+ *
51
+ * ─── THE HONEST BOUNDARY (NOT faked) ────────────────────────────────────────────────────────────────
52
+ * The one residue this combiner does NOT close is a receiver that is SIMULTANEOUSLY actively-malicious
53
+ * AND quantum: such a receiver could break the classical EC half with Shor (recovering `ss_ec_{1−c}`
54
+ * from the transcript) WHILE keeping both ML-KEM secrets (recovering `ss_kem_{1−c}`), and so assemble
55
+ * `k_{1−c}`. Closing THAT requires a base OT that is BOTH post-quantum AND malicious-secure in one
56
+ * primitive — i.e. an endemic/UC OT from Module-LWE with a CRS, which needs direct lattice module
57
+ * arithmetic (sampling / adding in ML-KEM's structured public-key space to plant a secretless slot).
58
+ * The opaque `ml_kem768` keygen/encaps/decaps interface this package is restricted to does NOT expose
59
+ * that arithmetic, and hand-rolling it is exactly the lattice crypto the module forbids. So this is the
60
+ * honest limit of what a KEM-opaque + EC combiner can reach; it is stated, not papered over.
61
+ *
62
+ * No hand-rolled crypto: the EC half goes through `malBaseRandomOT` (unchanged), the KEM half through
63
+ * `kemBaseOtMalRandom` (unchanged), and this module only hashes (SHA-256 RO) and XORs their outputs.
64
+ */
65
+ Object.defineProperty(exports, "__esModule", { value: true });
66
+ exports.HybridOtAbort = void 0;
67
+ exports.hybridSessionId = hybridSessionId;
68
+ exports.hybridTransferKey = hybridTransferKey;
69
+ exports.hybridSenderEncrypt = hybridSenderEncrypt;
70
+ exports.hybridReceiverRecover = hybridReceiverRecover;
71
+ exports.hybridBaseOT = hybridBaseOT;
72
+ exports.withHybridBaseOt = withHybridBaseOt;
73
+ const node_crypto_1 = require("node:crypto");
74
+ const ec_1 = require("./ec");
75
+ const ot_1 = require("./ot");
76
+ const kem_ot_1 = require("./kem-ot");
77
+ const csprng_1 = require("./csprng");
78
+ const TAG_HYBRID_SID = new TextEncoder().encode('atlas-pca-mpc/hybrid-ot-sid');
79
+ const TAG_HYBRID_KEY = new TextEncoder().encode('atlas-pca-mpc/hybrid-ot-transfer-key');
80
+ const TAG_HYBRID_COMMIT = new TextEncoder().encode('atlas-pca-mpc/hybrid-ot-msg-commit');
81
+ function sha256(...chunks) {
82
+ const h = (0, node_crypto_1.createHash)('sha256');
83
+ for (const c of chunks)
84
+ h.update(c);
85
+ return new Uint8Array(h.digest());
86
+ }
87
+ function u32(x) {
88
+ const b = new Uint8Array(4);
89
+ b[0] = (x >>> 24) & 0xff;
90
+ b[1] = (x >>> 16) & 0xff;
91
+ b[2] = (x >>> 8) & 0xff;
92
+ b[3] = x & 0xff;
93
+ return b;
94
+ }
95
+ function xorBytes(a, b) {
96
+ if (a.length !== b.length)
97
+ throw new Error('hybrid-ot: XOR length mismatch');
98
+ const out = new Uint8Array(a.length);
99
+ for (let i = 0; i < a.length; i++)
100
+ out[i] = a[i] ^ b[i];
101
+ return out;
102
+ }
103
+ function ctEqual(a, b) {
104
+ return a.length === b.length && (0, node_crypto_1.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
105
+ }
106
+ /** Raised when the hybrid combiner aborts: a sender ciphertext inconsistent with its commitment is caught. */
107
+ class HybridOtAbort extends Error {
108
+ constructor(message) {
109
+ super(message);
110
+ this.name = 'HybridOtAbort';
111
+ }
112
+ }
113
+ exports.HybridOtAbort = HybridOtAbort;
114
+ /**
115
+ * The COMMON session id binding both halves: `H(TAG ‖ kemSid ‖ encode(ecS))`. It folds the KEM
116
+ * sub-protocol's two-nonce session id (`rn, sn`) and the EC sub-protocol's point `S`, so neither party
117
+ * alone fixes it and secrets cannot be mixed across sessions. Both parties derive the same value from
118
+ * the public transcript.
119
+ */
120
+ function hybridSessionId(kemSid, ecS) {
121
+ return sha256(TAG_HYBRID_SID, kemSid, (0, ec_1.encodePoint)(ecS));
122
+ }
123
+ /**
124
+ * The hybrid transfer key for one slot: `H(TAG ‖ sid ‖ i ‖ slot ‖ ss_ec ‖ ss_kem)` expanded (SHA-256
125
+ * counter mode) to `len` bytes. Modelled as a random oracle: the key is pseudorandom unless BOTH
126
+ * `ss_ec` and `ss_kem` (for that exact slot/index/session) are supplied — the soundness crux of the
127
+ * combiner.
128
+ */
129
+ function hybridTransferKey(sid, index, slot, ssEc, ssKem, len) {
130
+ const out = new Uint8Array(len);
131
+ let produced = 0;
132
+ let ctr = 0;
133
+ while (produced < len) {
134
+ const block = sha256(TAG_HYBRID_KEY, sid, u32(index), Uint8Array.of(slot & 1), u32(ctr), ssEc, ssKem);
135
+ for (let k = 0; k < block.length && produced < len; k++)
136
+ out[produced++] = block[k];
137
+ ctr++;
138
+ }
139
+ return out;
140
+ }
141
+ /** Per-message RO commitment `com = H(TAG ‖ sid ‖ i ‖ slot ‖ msg ‖ key)`, bound to the session. */
142
+ function hybridCommit(sid, index, slot, msg, key) {
143
+ return sha256(TAG_HYBRID_COMMIT, sid, u32(index), Uint8Array.of(slot & 1), msg, key);
144
+ }
145
+ /**
146
+ * SENDER side of the combiner: given BOTH halves' per-slot shared secrets, derive both transfer keys,
147
+ * one-time-pad each message, and commit to each. `ecSecrets[i] = [ss_ec_0, ss_ec_1]` (the EC random-OT
148
+ * `senderPads`), `kemSecrets[i] = [ss_kem_0, ss_kem_1]` (the committed-KEM `senderSecrets`).
149
+ */
150
+ function hybridSenderEncrypt(sid, messages, ecSecrets, kemSecrets) {
151
+ if (messages.length !== ecSecrets.length || messages.length !== kemSecrets.length) {
152
+ throw new Error('hybridSenderEncrypt: messages/ecSecrets/kemSecrets length mismatch');
153
+ }
154
+ const encrypted = [];
155
+ const commitments = [];
156
+ for (let i = 0; i < messages.length; i++) {
157
+ const [m0, m1] = messages[i];
158
+ if (m0.length !== m1.length) {
159
+ throw new Error(`hybridSenderEncrypt (index ${i}): the two messages must be equal length`);
160
+ }
161
+ const k0 = hybridTransferKey(sid, i, 0, ecSecrets[i][0], kemSecrets[i][0], m0.length);
162
+ const k1 = hybridTransferKey(sid, i, 1, ecSecrets[i][1], kemSecrets[i][1], m1.length);
163
+ encrypted.push([xorBytes(m0, k0), xorBytes(m1, k1)]);
164
+ commitments.push([hybridCommit(sid, i, 0, m0, k0), hybridCommit(sid, i, 1, m1, k1)]);
165
+ }
166
+ return { encrypted, commitments };
167
+ }
168
+ /**
169
+ * RECEIVER side of the combiner: reconstruct ONLY the chosen transfer key (from the chosen EC and KEM
170
+ * secrets the receiver legitimately holds), unmask the chosen message, and CHECK the commitment.
171
+ * `ecRecv[i] = ss_ec_{choices[i]}` (the EC `recvPads`), `kemRecv[i] = ss_kem_{choices[i]}` (the
172
+ * committed-KEM `recvSecrets`). Aborts (`HybridOtAbort`) if the recovered message fails its commitment.
173
+ */
174
+ function hybridReceiverRecover(sid, choices, sender, ecRecv, kemRecv) {
175
+ const { encrypted, commitments } = sender;
176
+ if (encrypted.length !== choices.length ||
177
+ commitments.length !== choices.length ||
178
+ ecRecv.length !== choices.length ||
179
+ kemRecv.length !== choices.length) {
180
+ throw new Error('hybridReceiverRecover: length mismatch');
181
+ }
182
+ const received = [];
183
+ for (let i = 0; i < choices.length; i++) {
184
+ const c = choices[i] & 1;
185
+ const e = encrypted[i][c];
186
+ const key = hybridTransferKey(sid, i, c, ecRecv[i], kemRecv[i], e.length);
187
+ const m = xorBytes(e, key);
188
+ const check = hybridCommit(sid, i, c, m, key);
189
+ if (!ctEqual(check, commitments[i][c])) {
190
+ throw new HybridOtAbort(`hybrid base OT (index ${i}): sender ciphertext inconsistent with its commitment; aborting`);
191
+ }
192
+ received.push(m);
193
+ }
194
+ return received;
195
+ }
196
+ /**
197
+ * In-process combined simulator (both parties in one call), matching the shape of `baseOT`/`kemBaseOT`
198
+ * so it is a drop-in for `OtChannel`'s `baseOtFn` (`withHybridBaseOt`). Runs the malicious-secure EC
199
+ * base OT and the committed ML-KEM base OT on the SAME choices, binds both to a common `sid`, and
200
+ * combines their per-slot secrets into the transfer keys. A cheating party is caught by whichever
201
+ * half's check applies (EC proof/point validation, KEM commitment, or the hybrid ciphertext
202
+ * commitment). `tamper` injects malicious behaviour for tests.
203
+ */
204
+ function hybridBaseOT(messages, choices, rng = (0, csprng_1.secureOtRandom)(), tamper) {
205
+ if (messages.length !== choices.length) {
206
+ throw new Error('hybridBaseOT: messages/choices length mismatch');
207
+ }
208
+ // EC half: malicious-secure random OT with ALL checks ON (Schnorr proof + subgroup validation).
209
+ const ec = (0, ot_1.malBaseRandomOT)(choices, rng, tamper?.ec);
210
+ // KEM half: the committed/session-bound random OT (aborts on an opened-key/commitment mismatch).
211
+ const kem = (0, kem_ot_1.kemBaseOtMalRandom)(choices, rng, tamper?.kem);
212
+ const sid = hybridSessionId(kem.sid, ec.S);
213
+ const sender = hybridSenderEncrypt(sid, messages, ec.senderPads, kem.senderSecrets);
214
+ // A malicious hybrid SENDER may corrupt the ciphertexts on the wire (test-injected).
215
+ if (tamper?.tamperEncrypted)
216
+ tamper.tamperEncrypted(sender.encrypted);
217
+ const received = hybridReceiverRecover(sid, choices, sender, ec.recvPads, kem.recvSecrets);
218
+ return {
219
+ received,
220
+ transcript: {
221
+ sid,
222
+ ecS: ec.S,
223
+ kemSid: kem.sid,
224
+ encrypted: sender.encrypted,
225
+ commitments: sender.commitments,
226
+ },
227
+ };
228
+ }
229
+ /**
230
+ * Wire the HYBRID base OT into the KOS OT-extension: a drop-in for `withKemBaseOt`/`withKemBaseOtMal`
231
+ * that makes `OtChannel` run its κ setup base OTs over the combiner. The extension inherits the
232
+ * combiner's guarantee — malicious security vs a classical receiver, PQ sender-privacy vs a semi-honest
233
+ * quantum one, and PQ choice privacy — with everything above the base OT (IKNP matrix, KOS correlation
234
+ * check, correlation-robust hash) unchanged.
235
+ *
236
+ * const ch = new OtChannel(secureOtRandom(), withHybridBaseOt());
237
+ */
238
+ function withHybridBaseOt() {
239
+ return { baseOtFn: (m, c, r) => ({ received: hybridBaseOT(m, c, r).received }) };
240
+ }
241
+ //# sourceMappingURL=hybrid-ot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid-ot.js","sourceRoot":"","sources":["../src/hybrid-ot.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -24,4 +24,6 @@ export * from './gf128';
24
24
  export * from './csprng';
25
25
  export * from './ot';
26
26
  export * from './kem-ot';
27
+ export * from './hybrid-ot';
27
28
  export * from './mascot';
29
+ export * from './endemic-ot';
package/dist/index.js CHANGED
@@ -40,5 +40,7 @@ __exportStar(require("./gf128"), exports);
40
40
  __exportStar(require("./csprng"), exports);
41
41
  __exportStar(require("./ot"), exports);
42
42
  __exportStar(require("./kem-ot"), exports);
43
+ __exportStar(require("./hybrid-ot"), exports);
43
44
  __exportStar(require("./mascot"), exports);
45
+ __exportStar(require("./endemic-ot"), exports);
44
46
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;;;;;;;;;;;AAEH,0CAAwB;AACxB,4CAA0B;AAC1B,2CAAyB;AACzB,gDAA8B;AAC9B,0CAAwB;AACxB,4CAA0B;AAC1B,2CAAyB;AACzB,yCAAuB;AACvB,gDAA8B;AAC9B,uCAAqB;AACrB,0CAAwB;AACxB,2CAAyB;AACzB,uCAAqB;AACrB,2CAAyB;AACzB,2CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;;;;;;;;;;;AAEH,0CAAwB;AACxB,4CAA0B;AAC1B,2CAAyB;AACzB,gDAA8B;AAC9B,0CAAwB;AACxB,4CAA0B;AAC1B,2CAAyB;AACzB,yCAAuB;AACvB,gDAA8B;AAC9B,uCAAqB;AACrB,0CAAwB;AACxB,2CAAyB;AACzB,uCAAqB;AACrB,2CAAyB;AACzB,8CAA4B;AAC5B,2CAAyB;AACzB,+CAA6B"}
package/dist/kem-ot.d.ts CHANGED
@@ -43,18 +43,41 @@
43
43
  * rests on Module-LWE (ML-KEM-768, NIST security level ~3) and a hash in the ROM — no classical
44
44
  * discrete-log anywhere, which is the entire point of P3.
45
45
  *
46
- * ─── LIFTING TOWARD MALICIOUS (how this plugs into the stack) ──────────────────────────────────────
47
- * These base OTs feed the existing KOS OT-extension (`ot.ts`). KOS runs the base OTs with ROLES
48
- * INVERTED (the extension *sender* is the base *receiver*, holding the secret selector s) and then adds
49
- * the KOS correlation check over sacrifice rows, which catches an extension receiver that deviates. The
50
- * malicious surface a semi-honest base OT leaves open — a base receiver keeping both secrets — maps to
51
- * the extension *sender* learning the extension *receiver*'s seed pairs; KOS's consistency machinery and
52
- * the correlation-robust hash are what constrain that, exactly as documented for the IKNP/KOS layer.
53
- * HONEST CAVEAT: a semi-honest base OT does not give a fully malicious base OT on its own; a
54
- * deployment wanting malicious base security would add a base-OT consistency/commitment round (as the
55
- * EC `baseOT` wrapper does with its per-message RO commitment). The KOS layer above raises the overall
56
- * extension to malicious-with-abort against a cheating *extension receiver*, which is the attack IKNP is
57
- * vulnerable to; see `ot.ts` for that argument.
46
+ * ─── MALICIOUS-RECEIVER HARDENING (`kemBaseOtMal`, below) ──────────────────────────────────────────
47
+ * The semi-honest base OT above leaves two malicious-receiver surfaces. The committed variant
48
+ * `kemBaseOtMal` closes the one that CAN be closed from an opaque KEM, and documents — precisely,
49
+ * rather than faking a defence — the one that cannot:
50
+ *
51
+ * CLOSED (by an RO-commitment + session binding, mirroring the EC `baseOT`'s commit round):
52
+ * • ADAPTIVE / MALLEABLE / REPLAYED key material. The receiver first sends only a RO-commitment
53
+ * `com = H(rn ‖ pk_0 ‖ pk_1)` to its ordered public keys (and a receiver nonce `rn`); the sender
54
+ * replies with a fresh nonce `sn`; the parties set `sid = H(rn ‖ sn)`; only THEN does the receiver
55
+ * open `(pk_0, pk_1)` and the sender encapsulate, binding every pad to `(sid, com)`. The sender
56
+ * recomputes `com` from the opened keys and ABORTS on any mismatch. So the receiver must fix its
57
+ * keys BEFORE it learns `sn` (no adaptive/precomputed/related-key choice, no cross-session replay),
58
+ * and cannot open keys other than the ones it committed to — exactly the non-malleability the EC
59
+ * base OT gets from `R` being a single committed group element.
60
+ *
61
+ * NOT CLOSABLE from an OPAQUE ML-KEM, and WHY (an impossibility, not a shortcut):
62
+ * • A receiver that simply runs keygen TWICE and keeps BOTH secret keys learns both messages. To
63
+ * stop it the protocol would have to force one slot to a public key whose secret NOBODY knows
64
+ * (the Masny–Rindal "endemic OT" idea: `pk_{1−c}` derived in the public-key space from a random
65
+ * oracle / a common reference). That needs either (i) sampling / adding in ML-KEM's structured
66
+ * public-key space — reaching into the Module-LWE internals this module forbids (no hand-rolled
67
+ * lattice crypto), or (ii) a trusted setup / CRS. Crucially, the SAME decisional-Module-LWE
68
+ * indistinguishability that makes choice privacy hold (a real `pk` ≈ a uniform one) is what makes
69
+ * a cheating "both-keys-real" receiver UNDETECTABLE to the sender: it cannot tell a secretless
70
+ * slot from a real one. So this residue is intrinsic to the opaque primitive.
71
+ *
72
+ * ─── LIFTING THE RESIDUE (how this plugs into the stack) ───────────────────────────────────────────
73
+ * The residue is closed exactly where the EC stack closes its analogue: the KOS OT-extension (`ot.ts`).
74
+ * KOS runs the base OTs with ROLES INVERTED (the extension *sender* is the base *receiver*, holding the
75
+ * secret selector s) and adds the correlation check over sacrifice rows. A base receiver keeping both
76
+ * secrets maps to the extension *sender* learning the extension *receiver*'s seed pairs; the KOS
77
+ * consistency machinery + correlation-robust hash constrain that, raising the overall extension to
78
+ * malicious-with-abort against a cheating extension receiver — the attack IKNP is vulnerable to. So
79
+ * `kemBaseOtMal` is the right base layer: it hardens everything the opaque KEM allows (commitment /
80
+ * binding), and defers only the intrinsic residue to the layer that already handles it.
58
81
  *
59
82
  * No hand-rolled lattice crypto: all PQ operations go through `ml_kem768` (keygen/encapsulate/
60
83
  * decapsulate) as a black box. Randomness follows the module policy — a CSPRNG (`secureOtRandom`) by
@@ -135,3 +158,135 @@ export declare function kemBaseOT(messages: Array<[Uint8Array, Uint8Array]>, cho
135
158
  * const ch = new OtChannel(secureOtRandom(), withKemBaseOt());
136
159
  */
137
160
  export declare function withKemBaseOt(): ConstructorParameters<typeof OtChannel>[1];
161
+ /** Raised when the committed KEM base OT aborts: a cheating receiver (opened keys ≠ commitment) is caught. */
162
+ export declare class KemBaseOtAbort extends Error {
163
+ constructor(message: string);
164
+ }
165
+ /** Receiver round-1 message: a nonce + the per-transfer commitments (public keys NOT yet revealed). */
166
+ export interface KemMalCommitMessage {
167
+ /** Receiver nonce `rn`, bound into every commitment and into `sid`. */
168
+ rn: Uint8Array;
169
+ /** Per-transfer RO-commitment `com_i = H(rn ‖ i ‖ pk_0 ‖ pk_1)`. */
170
+ commitments: Uint8Array[];
171
+ }
172
+ /** Receiver round-3 message: the opening of the committed public keys. */
173
+ export interface KemMalOpenMessage {
174
+ /** For each transfer i, the ordered public keys `[pk_0, pk_1]` the sender encapsulates under. */
175
+ publicKeys: Array<[Uint8Array, Uint8Array]>;
176
+ }
177
+ /** Sender round-2 message: the sender nonce `sn` completing the session id. */
178
+ export interface KemMalSenderNonce {
179
+ sn: Uint8Array;
180
+ }
181
+ /** Sender round-4 message: ciphertexts + `(sid, com)`-bound padded messages. */
182
+ export interface KemMalSenderMessage {
183
+ ciphertexts: Array<[Uint8Array, Uint8Array]>;
184
+ encrypted: Array<[Uint8Array, Uint8Array]>;
185
+ }
186
+ /** The receiver's retained state across the committed flow. */
187
+ export interface KemMalReceiverState {
188
+ choices: number[];
189
+ /** The chosen secret key per transfer (`sk_{choices[i]}`); the non-chosen secret is discarded. */
190
+ secretKeys: Uint8Array[];
191
+ /** The ordered public keys (opened in round 3). */
192
+ publicKeys: Array<[Uint8Array, Uint8Array]>;
193
+ rn: Uint8Array;
194
+ }
195
+ /** A full committed KEM base-OT transcript, exposed for inspection/tests. */
196
+ export interface KemMalTranscript extends KemMalCommitMessage, KemMalOpenMessage, KemMalSenderNonce, KemMalSenderMessage {
197
+ }
198
+ export interface KemBaseOtMalResult {
199
+ received: Uint8Array[];
200
+ transcript: KemMalTranscript;
201
+ }
202
+ /**
203
+ * TEST-ONLY injection points modelling a malicious RECEIVER in the committed flow. Each hook mutates a
204
+ * value exactly where it crosses the wire, so the honest sender's checks run against the adversarial
205
+ * value.
206
+ */
207
+ export interface KemMalTamper {
208
+ /** Malicious receiver: open public keys DIFFERENT from the committed ones (→ sender abort). */
209
+ tamperOpenedKeys?: (publicKeys: Array<[Uint8Array, Uint8Array]>) => void;
210
+ /** Malicious receiver/MITM: tamper the sender nonce the receiver binds `sid` with (→ pad mismatch). */
211
+ tamperReceiverSid?: (sid: Uint8Array) => Uint8Array;
212
+ }
213
+ /**
214
+ * RECEIVER round 1: generate the keypairs (keeping only the chosen secret), draw a nonce `rn`, and send
215
+ * only the COMMITMENTS to the ordered public keys. Returns the round-1 wire message plus the retained
216
+ * state (which holds the not-yet-revealed public keys for the round-3 opening).
217
+ */
218
+ export declare function kemBaseOtMalCommit(choices: number[], rng?: OtRandom): {
219
+ message: KemMalCommitMessage;
220
+ state: KemMalReceiverState;
221
+ };
222
+ /** SENDER round 2: draw the sender nonce `sn` completing the session id. */
223
+ export declare function kemBaseOtMalSenderNonce(rng?: OtRandom): KemMalSenderNonce;
224
+ /** RECEIVER round 3: open the committed public keys (the sender will re-check them against `com`). */
225
+ export declare function kemBaseOtMalOpen(state: KemMalReceiverState): KemMalOpenMessage;
226
+ /**
227
+ * SENDER round 4: VERIFY the opened keys against the round-1 commitments (ABORT on mismatch), then
228
+ * encapsulate under each and one-time-pad each message with a `(sid, com)`-bound pad.
229
+ */
230
+ export declare function kemBaseOtMalSend(messages: Array<[Uint8Array, Uint8Array]>, commit: KemMalCommitMessage, senderNonce: KemMalSenderNonce, opening: KemMalOpenMessage, rng?: OtRandom): KemMalSenderMessage;
231
+ /**
232
+ * RECEIVER round 5: decapsulate ONLY the chosen ciphertext and unmask with the `(sid, com)`-bound pad.
233
+ */
234
+ export declare function kemBaseOtMalRecover(state: KemMalReceiverState, senderNonce: KemMalSenderNonce, senderMessage: KemMalSenderMessage, tamper?: KemMalTamper): Uint8Array[];
235
+ /**
236
+ * In-process combined simulator for the MALICIOUS-HARDENED committed KEM base OT, matching the shape of
237
+ * `kemBaseOT` so it is a drop-in for `OtChannel`'s `baseOtFn` (`withKemBaseOtMal`). Runs all five
238
+ * rounds; a cheating receiver (opened keys ≠ commitment) makes the sender throw `KemBaseOtAbort`. The
239
+ * `tamper` hook injects malicious-receiver behaviour for tests.
240
+ */
241
+ export declare function kemBaseOtMal(messages: Array<[Uint8Array, Uint8Array]>, choices: number[], rng?: OtRandom, tamper?: KemMalTamper): KemBaseOtMalResult;
242
+ /**
243
+ * Wire the MALICIOUS-HARDENED committed ML-KEM base OT into the KOS OT-extension — the recommended
244
+ * post-quantum base. Like `withKemBaseOt`, but the κ setup base OTs run the committed/session-bound
245
+ * protocol (and abort on a cheating receiver). Everything above the base OT is unchanged.
246
+ *
247
+ * const ch = new OtChannel(secureOtRandom(), withKemBaseOtMal());
248
+ */
249
+ export declare function withKemBaseOtMal(): ConstructorParameters<typeof OtChannel>[1];
250
+ /** Byte length of a derived committed-KEM random-OT shared secret. */
251
+ export declare const KEM_OT_SECRET_BYTES = 32;
252
+ /** Sender round-4 message of the committed KEM RANDOM OT: ciphertexts (wire) + both per-slot secrets (sender-local). */
253
+ export interface KemMalRandomSenderMessage {
254
+ /** For each transfer i, the ML-KEM ciphertexts `[ct_0, ct_1]` (the only part that crosses the wire). */
255
+ ciphertexts: Array<[Uint8Array, Uint8Array]>;
256
+ /** Sender-LOCAL: the two session-bound shared secrets `[ss_0, ss_1]` per transfer (never sent). */
257
+ senderSecrets: Array<[Uint8Array, Uint8Array]>;
258
+ }
259
+ /** A full committed KEM RANDOM-OT transcript + the two parties' shared-secret outputs. */
260
+ export interface KemBaseOtMalRandomResult {
261
+ /** Sender's per-slot shared secrets `[ss_0, ss_1]` per transfer (the sender learns BOTH). */
262
+ senderSecrets: Array<[Uint8Array, Uint8Array]>;
263
+ /** Receiver's chosen shared secret `ss_{choices[i]}` per transfer (the honest receiver learns ONE). */
264
+ recvSecrets: Uint8Array[];
265
+ /** The session id `sid = H(rn ‖ sn)` the secrets are bound to (public; both parties derive it). */
266
+ sid: Uint8Array;
267
+ transcript: {
268
+ rn: Uint8Array;
269
+ commitments: Uint8Array[];
270
+ sn: Uint8Array;
271
+ publicKeys: Array<[Uint8Array, Uint8Array]>;
272
+ ciphertexts: Array<[Uint8Array, Uint8Array]>;
273
+ };
274
+ }
275
+ /**
276
+ * SENDER round 4 (RANDOM variant): VERIFY the opened keys against the round-1 commitments (ABORT on
277
+ * mismatch), encapsulate under each, and derive BOTH per-slot session-bound shared secrets. No messages
278
+ * are padded — the two secrets per transfer ARE the random output.
279
+ */
280
+ export declare function kemBaseOtMalRandomSend(commit: KemMalCommitMessage, senderNonce: KemMalSenderNonce, opening: KemMalOpenMessage, rng?: OtRandom): KemMalRandomSenderMessage;
281
+ /**
282
+ * RECEIVER round 5 (RANDOM variant): decapsulate ONLY the chosen ciphertext and derive the chosen
283
+ * session-bound shared secret. The receiver never touches the non-chosen slot (it kept no secret for it).
284
+ */
285
+ export declare function kemBaseOtMalRandomRecover(state: KemMalReceiverState, senderNonce: KemMalSenderNonce, senderMessage: KemMalRandomSenderMessage, tamper?: KemMalTamper): Uint8Array[];
286
+ /**
287
+ * In-process combined simulator for the committed KEM RANDOM OT. Runs the same five rounds as
288
+ * `kemBaseOtMal` (commit → sender nonce → open → verify+encapsulate → recover) but outputs per-slot
289
+ * shared secrets instead of padded messages. A cheating receiver (opened keys ≠ commitment) makes the
290
+ * sender throw `KemBaseOtAbort`. The `tamper` hook injects malicious-receiver behaviour for tests.
291
+ */
292
+ export declare function kemBaseOtMalRandom(choices: number[], rng?: OtRandom, tamper?: KemMalTamper): KemBaseOtMalRandomResult;