@bcts/spqr 1.0.0-alpha.21

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/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@bcts/spqr",
3
+ "version": "1.0.0-alpha.21",
4
+ "type": "module",
5
+ "description": "Signal's Sparse Post-Quantum Ratchet (SPQR) for TypeScript",
6
+ "license": "AGPL-3.0-only",
7
+ "author": "Parity Technologies <admin@parity.io> (https://parity.io)",
8
+ "homepage": "https://bcts.dev",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/paritytech/bcts",
12
+ "directory": "packages/spqr"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/paritytech/bcts/issues"
16
+ },
17
+ "main": "dist/index.cjs",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.mts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.mts",
23
+ "import": "./dist/index.mjs",
24
+ "require": "./dist/index.cjs",
25
+ "default": "./dist/index.mjs"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
38
+ "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
39
+ "typecheck": "tsc --noEmit",
40
+ "clean": "rm -rf dist",
41
+ "docs": "typedoc",
42
+ "prepublishOnly": "npm run clean && npm run build && npm test"
43
+ },
44
+ "keywords": [
45
+ "spqr",
46
+ "post-quantum",
47
+ "ml-kem",
48
+ "ratchet",
49
+ "signal-protocol",
50
+ "erasure-coding",
51
+ "blockchain-commons"
52
+ ],
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "dependencies": {
57
+ "@bcts/crypto": "workspace:*",
58
+ "@bcts/rand": "workspace:*",
59
+ "@noble/hashes": "^2.0.1",
60
+ "@noble/post-quantum": "^0.5.4"
61
+ },
62
+ "devDependencies": {
63
+ "@bcts/eslint": "workspace:*",
64
+ "@bcts/tsconfig": "workspace:*",
65
+ "@eslint/js": "^10.0.1",
66
+ "@types/node": "^25.3.2",
67
+ "eslint": "^10.0.2",
68
+ "prettier": "^3.8.1",
69
+ "tsdown": "^0.20.3",
70
+ "typedoc": "^0.28.17",
71
+ "typescript": "^5.9.3",
72
+ "vitest": "^4.0.18"
73
+ }
74
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * SPQR Authenticator -- HMAC-SHA256 MAC for KEM exchanges.
6
+ *
7
+ * Ported from Signal's spqr crate: authenticator.rs
8
+ *
9
+ * The Authenticator produces and verifies MACs over ciphertext and header
10
+ * data using HMAC-SHA256. The internal rootKey and macKey are updated
11
+ * via HKDF at each epoch transition.
12
+ *
13
+ * All info strings and data formats MUST match the Rust implementation exactly.
14
+ */
15
+
16
+ import { hkdfSha256, hmacSha256 } from "./kdf.js";
17
+ import { concat, bigintToBE8, constantTimeEqual } from "./util.js";
18
+ import {
19
+ ZERO_SALT,
20
+ MAC_SIZE,
21
+ LABEL_AUTH_UPDATE,
22
+ LABEL_CT_MAC,
23
+ LABEL_HDR_MAC,
24
+ } from "./constants.js";
25
+ import { AuthenticatorError } from "./error.js";
26
+ import type { Epoch } from "./types.js";
27
+ import type { PbAuthenticator } from "./proto/pq-ratchet-types.js";
28
+
29
+ // Pre-encode label strings
30
+ const enc = new TextEncoder();
31
+ const AUTH_UPDATE_INFO = enc.encode(LABEL_AUTH_UPDATE);
32
+ const CT_MAC_PREFIX = enc.encode(LABEL_CT_MAC);
33
+ const HDR_MAC_PREFIX = enc.encode(LABEL_HDR_MAC);
34
+
35
+ export { MAC_SIZE };
36
+
37
+ /**
38
+ * Authenticator manages root_key and mac_key state for producing
39
+ * and verifying MACs over KEM ciphertext and headers.
40
+ *
41
+ * The update operation uses HKDF:
42
+ * IKM = [rootKey || key]
43
+ * Salt = ZERO_SALT (32 zeros)
44
+ * Info = LABEL_AUTH_UPDATE + epoch_be8
45
+ * Output: 64 bytes -> [0..32] = new rootKey, [32..64] = new macKey
46
+ *
47
+ * MAC operations use HMAC-SHA256:
48
+ * Key = macKey
49
+ * Data = [label_prefix || epoch_be8 || payload]
50
+ */
51
+ export class Authenticator {
52
+ private rootKey: Uint8Array;
53
+ private macKey: Uint8Array;
54
+
55
+ constructor(rootKey: Uint8Array, macKey: Uint8Array) {
56
+ this.rootKey = rootKey;
57
+ this.macKey = macKey;
58
+ }
59
+
60
+ /**
61
+ * Create a new Authenticator from a root key and initial epoch.
62
+ * Matches Rust: `Authenticator::new(root_key, ep)`
63
+ *
64
+ * Initializes with zero keys, then immediately updates with
65
+ * the provided root key and epoch.
66
+ */
67
+ static create(rootKey: Uint8Array, epoch: Epoch): Authenticator {
68
+ const auth = new Authenticator(new Uint8Array(32), new Uint8Array(32));
69
+ auth.update(epoch, rootKey);
70
+ return auth;
71
+ }
72
+
73
+ /**
74
+ * Update the authenticator with a new epoch and key material.
75
+ *
76
+ * HKDF(IKM=[rootKey||key], salt=ZERO_SALT, info=[label||epoch_be8], length=64)
77
+ *
78
+ * Output split: rootKey = [0..32], macKey = [32..64]
79
+ */
80
+ update(epoch: Epoch, key: Uint8Array): void {
81
+ // ikm = [root_key || key]
82
+ const ikm = concat(this.rootKey, key);
83
+
84
+ const epochBe8 = bigintToBE8(epoch);
85
+ const info = concat(AUTH_UPDATE_INFO, epochBe8);
86
+
87
+ const derived = hkdfSha256(ikm, ZERO_SALT, info, 64);
88
+ this.rootKey = derived.slice(0, 32);
89
+ this.macKey = derived.slice(32, 64);
90
+ }
91
+
92
+ /**
93
+ * Compute MAC over ciphertext.
94
+ *
95
+ * HMAC-SHA256(macKey, [LABEL_CT_MAC || epoch_be8 || ct])
96
+ */
97
+ macCt(epoch: Epoch, ct: Uint8Array): Uint8Array {
98
+ const epochBe8 = bigintToBE8(epoch);
99
+ const data = concat(CT_MAC_PREFIX, epochBe8, ct);
100
+ return hmacSha256(this.macKey, data);
101
+ }
102
+
103
+ /**
104
+ * Verify ciphertext MAC (constant-time comparison).
105
+ * Throws AuthenticatorError if the MAC does not match.
106
+ */
107
+ verifyCt(epoch: Epoch, ct: Uint8Array, expectedMac: Uint8Array): void {
108
+ const computed = this.macCt(epoch, ct);
109
+ if (!constantTimeEqual(computed, expectedMac)) {
110
+ throw new AuthenticatorError("Ciphertext MAC is invalid", "INVALID_CT_MAC");
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Compute MAC over header (encapsulation key header).
116
+ *
117
+ * HMAC-SHA256(macKey, [LABEL_HDR_MAC || epoch_be8 || hdr])
118
+ */
119
+ macHdr(epoch: Epoch, hdr: Uint8Array): Uint8Array {
120
+ const epochBe8 = bigintToBE8(epoch);
121
+ const data = concat(HDR_MAC_PREFIX, epochBe8, hdr);
122
+ return hmacSha256(this.macKey, data);
123
+ }
124
+
125
+ /**
126
+ * Verify header MAC (constant-time comparison).
127
+ * Throws AuthenticatorError if the MAC does not match.
128
+ */
129
+ verifyHdr(epoch: Epoch, hdr: Uint8Array, expectedMac: Uint8Array): void {
130
+ const computed = this.macHdr(epoch, hdr);
131
+ if (!constantTimeEqual(computed, expectedMac)) {
132
+ throw new AuthenticatorError("Encapsulation key MAC is invalid", "INVALID_HDR_MAC");
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Deep clone this authenticator.
138
+ */
139
+ clone(): Authenticator {
140
+ return new Authenticator(Uint8Array.from(this.rootKey), Uint8Array.from(this.macKey));
141
+ }
142
+
143
+ // ---- Protobuf serialization ----
144
+
145
+ /**
146
+ * Serialize to protobuf representation.
147
+ * Matches Rust Authenticator::into_pb().
148
+ */
149
+ toProto(): PbAuthenticator {
150
+ return {
151
+ rootKey: Uint8Array.from(this.rootKey),
152
+ macKey: Uint8Array.from(this.macKey),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Deserialize from protobuf representation.
158
+ * Matches Rust Authenticator::from_pb().
159
+ */
160
+ static fromProto(pb: PbAuthenticator): Authenticator {
161
+ return new Authenticator(Uint8Array.from(pb.rootKey), Uint8Array.from(pb.macKey));
162
+ }
163
+ }