@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/LICENSE +661 -0
- package/README.md +11 -0
- package/dist/index.cjs +4321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +4318 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +4312 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/authenticator.ts +163 -0
- package/src/chain.ts +522 -0
- package/src/constants.ts +90 -0
- package/src/encoding/gf.ts +190 -0
- package/src/encoding/index.ts +15 -0
- package/src/encoding/polynomial.ts +657 -0
- package/src/error.ts +75 -0
- package/src/incremental-mlkem768.ts +546 -0
- package/src/index.ts +415 -0
- package/src/kdf.ts +34 -0
- package/src/proto/index.ts +1376 -0
- package/src/proto/pq-ratchet-types.ts +195 -0
- package/src/types.ts +81 -0
- package/src/util.ts +61 -0
- package/src/v1/chunked/index.ts +60 -0
- package/src/v1/chunked/message.ts +257 -0
- package/src/v1/chunked/send-ct.ts +352 -0
- package/src/v1/chunked/send-ek.ts +285 -0
- package/src/v1/chunked/serialize.ts +278 -0
- package/src/v1/chunked/states.ts +399 -0
- package/src/v1/index.ts +9 -0
- package/src/v1/unchunked/index.ts +20 -0
- package/src/v1/unchunked/send-ct.ts +231 -0
- package/src/v1/unchunked/send-ek.ts +177 -0
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
|
+
}
|