@blamejs/blamejs-shop 0.1.34 → 0.1.35
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/CHANGELOG.md +2 -0
- package/README.md +1 -0
- package/lib/asset-manifest.json +7 -3
- package/lib/storefront.js +432 -4
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +89 -2
- package/lib/vendor/blamejs/index.js +3 -0
- package/lib/vendor/blamejs/lib/crdt.js +453 -0
- package/lib/vendor/blamejs/lib/crypto-xwing.js +213 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.3.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.13.4.json +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crdt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +120 -0
- package/package.json +2 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.crypto.xwing
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title X-Wing KEM
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* X-Wing is a general-purpose hybrid post-quantum / traditional key
|
|
9
|
+
* encapsulation mechanism: it runs ML-KEM-768 and X25519 side by side and
|
|
10
|
+
* binds their shared secrets with SHA3-256, so the resulting key stays
|
|
11
|
+
* secure as long as <em>either</em> ML-KEM-768 or X25519 holds. That is the
|
|
12
|
+
* conservative shape for migrating off classical ECDH today — a harvest-now-
|
|
13
|
+
* decrypt-later attacker must break the lattice KEM, and a hypothetical
|
|
14
|
+
* ML-KEM break still leaves X25519 standing.
|
|
15
|
+
*
|
|
16
|
+
* The construction follows
|
|
17
|
+
* <code>draft-connolly-cfrg-xwing-kem</code>. The combiner is frozen — it
|
|
18
|
+
* hashes the ML-KEM shared secret, the X25519 shared secret, the X25519
|
|
19
|
+
* ephemeral public key, the recipient's X25519 public key, and a fixed
|
|
20
|
+
* six-byte label — but the document is still an IETF Internet-Draft, so this
|
|
21
|
+
* primitive is marked <code>experimental</code> and sits beside the other
|
|
22
|
+
* pre-RFC post-quantum drafts (<code>b.crypto.hpke.pq</code>). The wire
|
|
23
|
+
* sizes are fixed: a 1216-byte public key (ML-KEM-768 1184 ‖ X25519 32), a
|
|
24
|
+
* 1120-byte ciphertext (ML-KEM-768 1088 ‖ X25519 32), a 32-byte decapsulation
|
|
25
|
+
* seed, and a 32-byte shared secret.
|
|
26
|
+
*
|
|
27
|
+
* X-Wing composes the framework's vendored ML-KEM-768 and X25519 plus
|
|
28
|
+
* SHA3 — it adds no new cryptographic core, only the standard combiner and
|
|
29
|
+
* wire framing.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* X-Wing hybrid PQ/T KEM (`b.crypto.xwing`) — ML-KEM-768 + X25519 bound by
|
|
33
|
+
* SHA3-256 per draft-connolly-cfrg-xwing-kem, secure if either component
|
|
34
|
+
* holds. 1216-byte key, 1120-byte ciphertext, 32-byte shared secret.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var nodeCrypto = require("node:crypto");
|
|
38
|
+
var pqc = require("./vendor/noble-post-quantum.cjs");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var XWingError = defineClass("XWingError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
var mlkem = pqc.ml_kem768;
|
|
44
|
+
|
|
45
|
+
// draft-connolly-cfrg-xwing-kem: the combiner label, ASCII "\./" + "/^\".
|
|
46
|
+
var XWING_LABEL = Buffer.from("5c2e2f2f5e5c", "hex");
|
|
47
|
+
|
|
48
|
+
// Component + composite sizes (bytes), fixed by the draft — protocol wire
|
|
49
|
+
// widths, not buffer-capacity tunables.
|
|
50
|
+
var ML_KEM_PK = 1184; // allow:raw-byte-literal — ML-KEM-768 public key
|
|
51
|
+
var ML_KEM_CT = 1088; // allow:raw-byte-literal — ML-KEM-768 ciphertext
|
|
52
|
+
var X25519_LEN = 32; // allow:raw-byte-literal — X25519 key/share length
|
|
53
|
+
var SEED_LEN = 32; // allow:raw-byte-literal — X-Wing seed length
|
|
54
|
+
var SS_LEN = 32; // allow:raw-byte-literal — shared-secret length
|
|
55
|
+
var PK_LEN = ML_KEM_PK + X25519_LEN; // 1216
|
|
56
|
+
var CT_LEN = ML_KEM_CT + X25519_LEN; // 1120
|
|
57
|
+
var MLKEM_SEED = 64; // allow:raw-byte-literal — d ‖ z for ML-KEM KeyGen_internal
|
|
58
|
+
var EXPAND_LEN = 96; // allow:raw-byte-literal — SHAKE256(seed) → d ‖ z ‖ sk_X
|
|
59
|
+
|
|
60
|
+
// X25519 raw-scalar helpers via fixed PKCS8 / SPKI DER prefixes (OID
|
|
61
|
+
// 1.3.101.110). Node clamps the scalar per RFC 7748 on use, matching X-Wing.
|
|
62
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
63
|
+
var X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
|
|
64
|
+
|
|
65
|
+
function _x25519Public(sk) {
|
|
66
|
+
var key = nodeCrypto.createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, sk]), format: "der", type: "pkcs8" });
|
|
67
|
+
var spki = nodeCrypto.createPublicKey(key).export({ format: "der", type: "spki" });
|
|
68
|
+
return spki.subarray(spki.length - X25519_LEN);
|
|
69
|
+
}
|
|
70
|
+
function _x25519Shared(sk, pk) {
|
|
71
|
+
return nodeCrypto.diffieHellman({
|
|
72
|
+
privateKey: nodeCrypto.createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, sk]), format: "der", type: "pkcs8" }),
|
|
73
|
+
publicKey: nodeCrypto.createPublicKey({ key: Buffer.concat([X25519_SPKI_PREFIX, pk]), format: "der", type: "spki" }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _shake256(buf, outLen) { return nodeCrypto.createHash("shake256", { outputLength: outLen }).update(buf).digest(); }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @primitive b.crypto.xwing.combiner
|
|
81
|
+
* @signature b.crypto.xwing.combiner(ssM, ssX, ctX, pkX)
|
|
82
|
+
* @since 0.13.3
|
|
83
|
+
* @status experimental
|
|
84
|
+
* @compliance soc2
|
|
85
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.decapsulate
|
|
86
|
+
*
|
|
87
|
+
* The X-Wing combiner: <code>SHA3-256(ssM ‖ ssX ‖ ctX ‖ pkX ‖ label)</code>,
|
|
88
|
+
* where the label is the fixed six bytes the draft defines. Exposed for
|
|
89
|
+
* advanced use and known-answer testing; <code>encapsulate</code> and
|
|
90
|
+
* <code>decapsulate</code> call it internally. Each input must be 32 bytes.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* var ss = b.crypto.xwing.combiner(ssMlkem, ssX25519, ephPub, recipientPub);
|
|
94
|
+
* // → 32-byte shared secret
|
|
95
|
+
*/
|
|
96
|
+
function combiner(ssM, ssX, ctX, pkX) {
|
|
97
|
+
[["ssM", ssM, SS_LEN], ["ssX", ssX, X25519_LEN], ["ctX", ctX, X25519_LEN], ["pkX", pkX, X25519_LEN]].forEach(function (t) {
|
|
98
|
+
// ML-KEM outputs are Uint8Array; X25519 outputs are Buffer — accept both.
|
|
99
|
+
if (!(Buffer.isBuffer(t[1]) || t[1] instanceof Uint8Array) || t[1].length !== t[2]) throw new XWingError("xwing/bad-input", "xwing.combiner: " + t[0] + " must be a " + t[2] + "-byte byte array");
|
|
100
|
+
});
|
|
101
|
+
return nodeCrypto.createHash("sha3-256").update(Buffer.concat([ssM, ssX, ctX, pkX, XWING_LABEL])).digest();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expand a 32-byte seed into ML-KEM key material + the X25519 scalar.
|
|
105
|
+
function _expand(seed) {
|
|
106
|
+
var e = _shake256(seed, EXPAND_LEN);
|
|
107
|
+
var kp = mlkem.keygen(e.subarray(0, MLKEM_SEED)); // KeyGen_internal(d, z)
|
|
108
|
+
var skX = e.subarray(MLKEM_SEED, EXPAND_LEN);
|
|
109
|
+
return { skM: kp.secretKey, pkM: kp.publicKey, skX: skX, pkX: _x25519Public(skX) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @primitive b.crypto.xwing.keygen
|
|
114
|
+
* @signature b.crypto.xwing.keygen(seed?)
|
|
115
|
+
* @since 0.13.3
|
|
116
|
+
* @status experimental
|
|
117
|
+
* @compliance soc2
|
|
118
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.decapsulate
|
|
119
|
+
*
|
|
120
|
+
* Generate an X-Wing keypair. The decapsulation key is a 32-byte seed (store
|
|
121
|
+
* this); the encapsulation key is the 1216-byte public key to publish. Pass a
|
|
122
|
+
* 32-byte <code>seed</code> for deterministic generation, or omit it for a
|
|
123
|
+
* random key.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* var kp = b.crypto.xwing.keygen();
|
|
127
|
+
* kp.publicKey.length; // → 1216
|
|
128
|
+
* kp.secretKey.length; // → 32 (the seed — keep it secret)
|
|
129
|
+
*/
|
|
130
|
+
function keygen(seed) {
|
|
131
|
+
if (seed == null) seed = nodeCrypto.randomBytes(SEED_LEN);
|
|
132
|
+
if (!Buffer.isBuffer(seed) || seed.length !== SEED_LEN) throw new XWingError("xwing/bad-seed", "xwing.keygen: seed must be a " + SEED_LEN + "-byte Buffer");
|
|
133
|
+
var k = _expand(seed);
|
|
134
|
+
return { publicKey: Buffer.concat([k.pkM, k.pkX]), secretKey: Buffer.from(seed) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.crypto.xwing.encapsulate
|
|
139
|
+
* @signature b.crypto.xwing.encapsulate(publicKey, eseed?)
|
|
140
|
+
* @since 0.13.3
|
|
141
|
+
* @status experimental
|
|
142
|
+
* @compliance soc2
|
|
143
|
+
* @related b.crypto.xwing.decapsulate, b.crypto.xwing.keygen
|
|
144
|
+
*
|
|
145
|
+
* Encapsulate to a 1216-byte X-Wing public key. Returns the 1120-byte
|
|
146
|
+
* <code>ciphertext</code> to send and the 32-byte <code>sharedSecret</code> to
|
|
147
|
+
* key a symmetric cipher with. Pass a 64-byte <code>eseed</code>
|
|
148
|
+
* (X25519 ephemeral scalar ‖ ML-KEM coins) for deterministic encapsulation, or
|
|
149
|
+
* omit it for fresh randomness.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* var enc = b.crypto.xwing.encapsulate(recipientPublicKey);
|
|
153
|
+
* enc.ciphertext.length; // → 1120
|
|
154
|
+
* enc.sharedSecret.length; // → 32
|
|
155
|
+
*/
|
|
156
|
+
function encapsulate(publicKey, eseed) {
|
|
157
|
+
if (!Buffer.isBuffer(publicKey) || publicKey.length !== PK_LEN) throw new XWingError("xwing/bad-public-key", "xwing.encapsulate: publicKey must be a " + PK_LEN + "-byte Buffer");
|
|
158
|
+
var pkM = publicKey.subarray(0, ML_KEM_PK);
|
|
159
|
+
var pkX = publicKey.subarray(ML_KEM_PK, PK_LEN);
|
|
160
|
+
var ekX, mlkemCoins = null;
|
|
161
|
+
if (eseed == null) {
|
|
162
|
+
ekX = nodeCrypto.randomBytes(X25519_LEN);
|
|
163
|
+
} else {
|
|
164
|
+
if (!Buffer.isBuffer(eseed) || eseed.length !== 2 * X25519_LEN) throw new XWingError("xwing/bad-eseed", "xwing.encapsulate: eseed must be a " + (2 * X25519_LEN) + "-byte Buffer");
|
|
165
|
+
// draft EncapsulateDerand: eseed[0:32] = ML-KEM coins, eseed[32:64] = X25519
|
|
166
|
+
// ephemeral scalar. This order matches the draft's test vectors.
|
|
167
|
+
mlkemCoins = eseed.subarray(0, X25519_LEN);
|
|
168
|
+
ekX = eseed.subarray(X25519_LEN, 2 * X25519_LEN);
|
|
169
|
+
}
|
|
170
|
+
var ctX = _x25519Public(ekX);
|
|
171
|
+
var ssX = _x25519Shared(ekX, pkX);
|
|
172
|
+
var kem = mlkemCoins ? mlkem.encapsulate(pkM, mlkemCoins) : mlkem.encapsulate(pkM);
|
|
173
|
+
var ss = combiner(kem.sharedSecret, ssX, ctX, pkX);
|
|
174
|
+
return { ciphertext: Buffer.concat([kem.cipherText, ctX]), sharedSecret: ss };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @primitive b.crypto.xwing.decapsulate
|
|
179
|
+
* @signature b.crypto.xwing.decapsulate(secretKey, ciphertext)
|
|
180
|
+
* @since 0.13.3
|
|
181
|
+
* @status experimental
|
|
182
|
+
* @compliance soc2
|
|
183
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.keygen
|
|
184
|
+
*
|
|
185
|
+
* Recover the 32-byte shared secret from a 1120-byte X-Wing ciphertext using
|
|
186
|
+
* the 32-byte decapsulation seed. ML-KEM-768's implicit-rejection means a
|
|
187
|
+
* tampered ciphertext yields a different (still 32-byte) secret rather than an
|
|
188
|
+
* error, so never branch on success — derive keys and let the AEAD tag fail.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* var ss = b.crypto.xwing.decapsulate(kp.secretKey, enc.ciphertext);
|
|
192
|
+
* ss.equals(enc.sharedSecret); // → true
|
|
193
|
+
*/
|
|
194
|
+
function decapsulate(secretKey, ciphertext) {
|
|
195
|
+
if (!Buffer.isBuffer(secretKey) || secretKey.length !== SEED_LEN) throw new XWingError("xwing/bad-seed", "xwing.decapsulate: secretKey must be a " + SEED_LEN + "-byte Buffer");
|
|
196
|
+
if (!Buffer.isBuffer(ciphertext) || ciphertext.length !== CT_LEN) throw new XWingError("xwing/bad-ciphertext", "xwing.decapsulate: ciphertext must be a " + CT_LEN + "-byte Buffer");
|
|
197
|
+
var k = _expand(secretKey);
|
|
198
|
+
var ctM = ciphertext.subarray(0, ML_KEM_CT);
|
|
199
|
+
var ctX = ciphertext.subarray(ML_KEM_CT, CT_LEN);
|
|
200
|
+
var ssM = mlkem.decapsulate(ctM, k.skM);
|
|
201
|
+
var ssX = _x25519Shared(k.skX, ctX);
|
|
202
|
+
return combiner(ssM, ssX, ctX, k.pkX);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
NAME: "X-Wing",
|
|
207
|
+
keygen: keygen,
|
|
208
|
+
encapsulate: encapsulate,
|
|
209
|
+
decapsulate: decapsulate,
|
|
210
|
+
combiner: combiner,
|
|
211
|
+
SIZES: { publicKey: PK_LEN, ciphertext: CT_LEN, secretKey: SEED_LEN, sharedSecret: SS_LEN },
|
|
212
|
+
XWingError: XWingError,
|
|
213
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.3",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.crypto.xwing` — X-Wing hybrid post-quantum KEM",
|
|
6
|
+
"summary": "b.crypto.xwing adds the X-Wing hybrid key-encapsulation mechanism (draft-connolly-cfrg-xwing-kem): it runs ML-KEM-768 and X25519 side by side and binds their shared secrets with SHA3-256, so an encapsulated key stays secure as long as either ML-KEM-768 or X25519 holds. That is the conservative shape for moving off classical ECDH today — a harvest-now-decrypt-later attacker must break the lattice KEM, and a hypothetical ML-KEM break still leaves X25519 standing. keygen() produces a 32-byte decapsulation seed and a 1216-byte public key; encapsulate(publicKey) returns a 1120-byte ciphertext and a 32-byte shared secret; decapsulate(secretKey, ciphertext) recovers it. The X-Wing combiner is frozen, but its specification is still an IETF Internet-Draft, so this primitive is marked experimental and sits beside the existing pre-RFC post-quantum HPKE drafts; it composes the framework's vendored ML-KEM-768 and X25519 with SHA3 and adds no new cryptographic core. The combiner is known-answer-tested byte-for-byte against the draft's definition.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.crypto.xwing` — X-Wing hybrid PQ/T KEM (experimental)",
|
|
13
|
+
"body": "`keygen(seed?)` → `{ publicKey (1216 B), secretKey (32-byte seed) }`; `encapsulate(publicKey, eseed?)` → `{ ciphertext (1120 B), sharedSecret (32 B) }`; `decapsulate(secretKey, ciphertext)` → the 32-byte shared secret. Both `keygen` and `encapsulate` accept an optional seed for deterministic operation. The combiner — `SHA3-256(ssMLKEM ‖ ssX25519 ‖ ctX25519 ‖ pkX25519 ‖ label)` — is exposed as `combiner` for advanced use. Marked `experimental` while draft-connolly-cfrg-xwing-kem remains an Internet-Draft; the algorithm itself is frozen."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.4",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.crdt` — conflict-free replicated data types",
|
|
6
|
+
"summary": "b.crdt adds state-based Conflict-free Replicated Data Types: data structures that independent replicas update without coordination and still converge to the same value once they have exchanged state. Each type's merge is a join over a semilattice — commutative, associative, and idempotent — so replicas can merge in any order, any number of times, and agree, which makes these the substrate for active/active cluster state, offline-first clients that reconcile on reconnect, and eventually-consistent counters, sets, and maps. The release ships the full state-based family: grow-only and positive-negative counters (gCounter / pnCounter), grow-only, two-phase, and observed-remove sets (gSet / twoPSet / orSet), a last-write-wins register (lwwRegister), and an observed-remove map (orMap). Every type exposes the same contract — local mutators, merge(other) that returns a converged instance without mutating either operand, value() for the materialized value, and state() / fromState() for a JSON-serializable form to snapshot via b.archive or b.backup or ship to a peer — and carries a replicaId so per-replica contributions stay distinct.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.crdt` — state-based CvRDT counters, sets, register, and map",
|
|
13
|
+
"body": "`b.crdt.gCounter` / `pnCounter` (grow-only and increment/decrement counters), `b.crdt.gSet` / `twoPSet` / `orSet` (grow-only, two-phase, and observed-remove sets — `orSet` supports re-add and resolves a concurrent add-vs-remove as add-wins), `b.crdt.lwwRegister` (last-write-wins with a deterministic replicaId tie-break), and `b.crdt.orMap` (observed-remove keys with last-write-wins values). Each exposes `merge` / `value` / `state` / `fromState` and converges by the CvRDT laws. `orSet` and `orMap` accept `tombstoneRetention` to bound tombstone memory against a remove flood."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"outOfScope": [
|
|
19
|
+
"Operation-based sequence CRDTs (RGA) — a different formal class (CmRDT) requiring a causal-delivery channel; ships when collaborative-text / ordered-list editing is needed.",
|
|
20
|
+
"Delta-state mutators — a bandwidth optimization over full-state merge; ships under large-state replication pressure.",
|
|
21
|
+
"The event-bus replicator for live multi-node auto-sync — the state-based types merge manually standalone; ships when live multi-node synchronization is wired, together with the causal-delivery option and its detector."
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.crdt (state-based CvRDTs).
|
|
4
|
+
*
|
|
5
|
+
* Oracle: the CRDT correctness laws themselves. A state-based CvRDT's merge is
|
|
6
|
+
* a join over a semilattice, so for every type merge must be commutative,
|
|
7
|
+
* associative, and idempotent, and two replicas that apply concurrent ops then
|
|
8
|
+
* merge must converge to the same value. Those laws ARE the specification
|
|
9
|
+
* (Shapiro et al.); each is asserted below alongside worked examples.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
var b = require("../../index");
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var check = helpers.check;
|
|
15
|
+
var crdt = b.crdt;
|
|
16
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
17
|
+
function val(c) { return JSON.stringify(c.value()); }
|
|
18
|
+
|
|
19
|
+
// Assert the three merge laws for a type, given three instances carrying
|
|
20
|
+
// concurrent (disjoint-replica) ops.
|
|
21
|
+
function laws(label, a, c, d) {
|
|
22
|
+
check(label + ": commutative", val(a.merge(c)) === val(c.merge(a)));
|
|
23
|
+
check(label + ": associative", val(a.merge(c).merge(d)) === val(a.merge(c.merge(d))));
|
|
24
|
+
check(label + ": idempotent", val(a.merge(a)) === val(a));
|
|
25
|
+
// Convergence: cross-merge of two replicas lands on one value both ways.
|
|
26
|
+
check(label + ": converges", val(a.merge(c)) === val(c.merge(a)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function testSurface() {
|
|
30
|
+
// Reference each primitive by its full b.crdt.* path (coverage gate).
|
|
31
|
+
check("b.crdt.gCounter is a factory", typeof b.crdt.gCounter === "function" && typeof b.crdt.gCounter.fromState === "function");
|
|
32
|
+
check("b.crdt.pnCounter is a factory", typeof b.crdt.pnCounter === "function" && typeof b.crdt.pnCounter.fromState === "function");
|
|
33
|
+
check("b.crdt.gSet is a factory", typeof b.crdt.gSet === "function" && typeof b.crdt.gSet.fromState === "function");
|
|
34
|
+
check("b.crdt.twoPSet is a factory", typeof b.crdt.twoPSet === "function" && typeof b.crdt.twoPSet.fromState === "function");
|
|
35
|
+
check("b.crdt.orSet is a factory", typeof b.crdt.orSet === "function" && typeof b.crdt.orSet.fromState === "function");
|
|
36
|
+
check("b.crdt.lwwRegister is a factory", typeof b.crdt.lwwRegister === "function" && typeof b.crdt.lwwRegister.fromState === "function");
|
|
37
|
+
check("b.crdt.orMap is a factory", typeof b.crdt.orMap === "function" && typeof b.crdt.orMap.fromState === "function");
|
|
38
|
+
check("b.crdt.CrdtError is a class", typeof b.crdt.CrdtError === "function");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function testGCounter() {
|
|
42
|
+
var a = crdt.gCounter({ replicaId: "a" }).inc(3);
|
|
43
|
+
var c = crdt.gCounter({ replicaId: "c" }).inc(5);
|
|
44
|
+
var d = crdt.gCounter({ replicaId: "d" }).inc(1);
|
|
45
|
+
check("gCounter sum across replicas", a.merge(c).value() === 8);
|
|
46
|
+
laws("gCounter", a, c, d);
|
|
47
|
+
check("gCounter inc default is 1", crdt.gCounter({ replicaId: "a" }).inc().value() === 1);
|
|
48
|
+
check("gCounter rejects negative inc", code(function () { crdt.gCounter().inc(-1); }) === "crdt/bad-value");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function testPNCounter() {
|
|
52
|
+
var a = crdt.pnCounter({ replicaId: "a" }).inc(5).dec(2);
|
|
53
|
+
var c = crdt.pnCounter({ replicaId: "c" }).inc(1);
|
|
54
|
+
var d = crdt.pnCounter({ replicaId: "d" }).dec(3);
|
|
55
|
+
check("pnCounter value", a.value() === 3);
|
|
56
|
+
check("pnCounter merged value", a.merge(c).merge(d).value() === 1);
|
|
57
|
+
laws("pnCounter", a, c, d);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function testGSet() {
|
|
61
|
+
var a = crdt.gSet({ replicaId: "a" }).add("x").add("y");
|
|
62
|
+
var c = crdt.gSet({ replicaId: "c" }).add("z");
|
|
63
|
+
var d = crdt.gSet({ replicaId: "d" }).add("x");
|
|
64
|
+
check("gSet union", val(a.merge(c)) === JSON.stringify(["x", "y", "z"]));
|
|
65
|
+
check("gSet has", a.has("x") && !a.has("q"));
|
|
66
|
+
check("gSet supports structured elements", crdt.gSet().add({ k: 1 }).has({ k: 1 }));
|
|
67
|
+
laws("gSet", a, c, d);
|
|
68
|
+
// value() order must converge for structured elements regardless of merge
|
|
69
|
+
// order (sorted by the encoded key, not the decoded value).
|
|
70
|
+
var s1 = crdt.gSet().add({ id: 2 }).add({ id: 1 });
|
|
71
|
+
var s2 = crdt.gSet().add({ id: 3 });
|
|
72
|
+
check("gSet structured-element value order converges", JSON.stringify(s1.merge(s2).value()) === JSON.stringify(s2.merge(s1).value()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function testTwoPSet() {
|
|
76
|
+
var s = crdt.twoPSet().add("a").add("b").remove("a");
|
|
77
|
+
check("twoPSet remove", val(s) === JSON.stringify(["b"]));
|
|
78
|
+
check("twoPSet remove-wins (no re-add)", !s.add("a").has("a"));
|
|
79
|
+
var a = crdt.twoPSet({ replicaId: "a" }).add("x").remove("x");
|
|
80
|
+
var c = crdt.twoPSet({ replicaId: "c" }).add("y");
|
|
81
|
+
var d = crdt.twoPSet({ replicaId: "d" }).add("z");
|
|
82
|
+
laws("twoPSet", a, c, d);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function testORSet() {
|
|
86
|
+
// Concurrent re-add survives a remove that did not observe it.
|
|
87
|
+
var a = crdt.orSet({ replicaId: "a" }).add("x").add("y");
|
|
88
|
+
var c = crdt.orSet.fromState(a.state(), { replicaId: "c" });
|
|
89
|
+
a.remove("x");
|
|
90
|
+
c.add("x");
|
|
91
|
+
check("orSet concurrent re-add survives remove", a.merge(c).value().indexOf("x") !== -1);
|
|
92
|
+
// A remove that observed the add wins.
|
|
93
|
+
var s = crdt.orSet().add("z");
|
|
94
|
+
check("orSet observed remove drops element", s.remove("z").value().indexOf("z") === -1);
|
|
95
|
+
laws("orSet", crdt.orSet({ replicaId: "a" }).add("p"), crdt.orSet({ replicaId: "c" }).add("q"), crdt.orSet({ replicaId: "d" }).add("r"));
|
|
96
|
+
// tombstoneRetention cap is accepted and bounds the tombstone set.
|
|
97
|
+
var capped = crdt.orSet({ replicaId: "a", tombstoneRetention: 2 });
|
|
98
|
+
capped.add("1").add("2").add("3").remove("1").remove("2").remove("3");
|
|
99
|
+
check("orSet tombstoneRetention bounds the set", capped.state().tombstones.length <= 2);
|
|
100
|
+
check("orSet rejects bad tombstoneRetention", code(function () { crdt.orSet({ tombstoneRetention: -1 }); }) === "crdt/bad-value");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function testLWWRegister() {
|
|
104
|
+
var a = crdt.lwwRegister({ replicaId: "a" }).set("first", 1);
|
|
105
|
+
var c = crdt.lwwRegister({ replicaId: "c" }).set("second", 2);
|
|
106
|
+
check("lwwRegister higher ts wins", a.merge(c).value() === "second");
|
|
107
|
+
// Tie-break by replicaId (deterministic).
|
|
108
|
+
var r1 = crdt.lwwRegister({ replicaId: "a" }).set("a", 100);
|
|
109
|
+
var r2 = crdt.lwwRegister({ replicaId: "c" }).set("c", 100);
|
|
110
|
+
check("lwwRegister tie-break by replicaId", r1.merge(r2).value() === "c" && r2.merge(r1).value() === "c");
|
|
111
|
+
laws("lwwRegister",
|
|
112
|
+
crdt.lwwRegister({ replicaId: "a" }).set("x", 5),
|
|
113
|
+
crdt.lwwRegister({ replicaId: "c" }).set("y", 9),
|
|
114
|
+
crdt.lwwRegister({ replicaId: "d" }).set("z", 3));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function testORMap() {
|
|
118
|
+
var a = crdt.orMap({ replicaId: "a" }).set("k1", "v1", 10);
|
|
119
|
+
var c = crdt.orMap({ replicaId: "c" }).set("k1", "v2", 20).set("k2", "x", 5);
|
|
120
|
+
var merged = a.merge(c);
|
|
121
|
+
check("orMap concurrent key write resolves LWW", merged.get("k1") === "v2");
|
|
122
|
+
check("orMap disjoint keys both present", merged.has("k2") && val(merged) === JSON.stringify({ k1: "v2", k2: "x" }));
|
|
123
|
+
check("orMap commutative", val(a.merge(c)) === val(c.merge(a)));
|
|
124
|
+
// Remove a key, then converge.
|
|
125
|
+
var p = crdt.orMap.fromState(merged.state(), { replicaId: "a" }).remove("k2");
|
|
126
|
+
check("orMap remove converges", val(p.merge(merged)) === JSON.stringify({ k1: "v2" }));
|
|
127
|
+
check("orMap rejects non-string key", code(function () { crdt.orMap().set(5, "v"); }) === "crdt/bad-key");
|
|
128
|
+
// Removing a key clears its register, so a re-add starts clean — a later set
|
|
129
|
+
// with a lower timestamp than the pre-remove value still wins on this replica.
|
|
130
|
+
var rr = crdt.orMap({ replicaId: "a" }).set("k", "old", 100);
|
|
131
|
+
rr.remove("k");
|
|
132
|
+
rr.set("k", "new", 50); // lower ts than the removed "old"@100
|
|
133
|
+
check("orMap remove clears register so re-add wins", rr.get("k") === "new");
|
|
134
|
+
check("orMap re-add appears in value", JSON.stringify(rr.value()) === JSON.stringify({ k: "new" }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function testStateRoundTrip() {
|
|
138
|
+
["gCounter", "pnCounter", "gSet", "twoPSet", "orSet", "lwwRegister", "orMap"].forEach(function (t) {
|
|
139
|
+
var inst = crdt[t]({ replicaId: "a" });
|
|
140
|
+
var s = inst.state();
|
|
141
|
+
check(t + ": fromState round-trips", JSON.stringify(crdt[t].fromState(s, { replicaId: "a" }).state()) === JSON.stringify(s));
|
|
142
|
+
check(t + ": fromState rejects a mismatched type", code(function () { crdt[t].fromState({ type: "WRONG" }); }) === "crdt/type-mismatch");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function run() {
|
|
147
|
+
testSurface();
|
|
148
|
+
testGCounter();
|
|
149
|
+
testPNCounter();
|
|
150
|
+
testGSet();
|
|
151
|
+
testTwoPSet();
|
|
152
|
+
testORSet();
|
|
153
|
+
testLWWRegister();
|
|
154
|
+
testORMap();
|
|
155
|
+
testStateRoundTrip();
|
|
156
|
+
}
|
|
157
|
+
module.exports = { run: run };
|
|
158
|
+
if (require.main === module) { run().then(function () { console.log("[crdt] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.crypto.xwing (X-Wing hybrid PQ/T KEM, draft-connolly-cfrg-xwing-kem).
|
|
4
|
+
*
|
|
5
|
+
* Oracle. The X-Wing-specific contribution is the combiner — SHA3-256 over a
|
|
6
|
+
* fixed concatenation plus a fixed label — so that is known-answer-tested
|
|
7
|
+
* byte-for-byte against an independent SHA3-256. The ML-KEM-768 and X25519
|
|
8
|
+
* halves are pre-validated vendored primitives; the composition is checked by
|
|
9
|
+
* full encaps → decaps agreement, by deterministic reproducibility from fixed
|
|
10
|
+
* seeds (regression-anchored to pinned digests), and by implicit-rejection
|
|
11
|
+
* behaviour on a tampered ciphertext.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var nodeCrypto = require("crypto");
|
|
15
|
+
var b = require("../../index");
|
|
16
|
+
var helpers = require("../helpers");
|
|
17
|
+
var check = helpers.check;
|
|
18
|
+
function code(fn) { try { fn(); return "NO-THROW"; } catch (e) { return e.code; } }
|
|
19
|
+
|
|
20
|
+
var x = b.crypto.xwing;
|
|
21
|
+
|
|
22
|
+
function testSurface() {
|
|
23
|
+
check("xwing.keygen is fn", typeof x.keygen === "function");
|
|
24
|
+
check("xwing.encapsulate is fn", typeof x.encapsulate === "function");
|
|
25
|
+
check("xwing.decapsulate is fn", typeof x.decapsulate === "function");
|
|
26
|
+
check("xwing.combiner is fn", typeof x.combiner === "function");
|
|
27
|
+
check("xwing.NAME is X-Wing", x.NAME === "X-Wing");
|
|
28
|
+
check("xwing.XWingError is class", typeof x.XWingError === "function");
|
|
29
|
+
check("SIZES pk 1216", x.SIZES.publicKey === 1216);
|
|
30
|
+
check("SIZES ct 1120", x.SIZES.ciphertext === 1120);
|
|
31
|
+
check("SIZES sk 32", x.SIZES.secretKey === 32);
|
|
32
|
+
check("SIZES ss 32", x.SIZES.sharedSecret === 32);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testCombinerKAT() {
|
|
36
|
+
// Byte-exact against the draft: SHA3-256(ssM ‖ ssX ‖ ctX ‖ pkX ‖ label),
|
|
37
|
+
// label = the six bytes 5c2e2f2f5e5c ("\./" "/^\").
|
|
38
|
+
var ssM = Buffer.alloc(32, 0x11), ssX = Buffer.alloc(32, 0x22),
|
|
39
|
+
ctX = Buffer.alloc(32, 0x33), pkX = Buffer.alloc(32, 0x44);
|
|
40
|
+
var ref = nodeCrypto.createHash("sha3-256")
|
|
41
|
+
.update(Buffer.concat([ssM, ssX, ctX, pkX, Buffer.from("5c2e2f2f5e5c", "hex")])).digest();
|
|
42
|
+
check("combiner matches independent SHA3-256 + label byte-for-byte", x.combiner(ssM, ssX, ctX, pkX).equals(ref));
|
|
43
|
+
// Order matters: swapping two inputs changes the output.
|
|
44
|
+
check("combiner is order-sensitive", !x.combiner(ssX, ssM, ctX, pkX).equals(ref));
|
|
45
|
+
check("combiner rejects a short input", code(function () { x.combiner(Buffer.alloc(31), ssX, ctX, pkX); }) === "xwing/bad-input");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function testSizes() {
|
|
49
|
+
var kp = x.keygen();
|
|
50
|
+
check("public key is 1216 bytes", kp.publicKey.length === 1216);
|
|
51
|
+
check("secret key (seed) is 32 bytes", kp.secretKey.length === 32);
|
|
52
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
53
|
+
check("ciphertext is 1120 bytes", enc.ciphertext.length === 1120);
|
|
54
|
+
check("shared secret is 32 bytes", enc.sharedSecret.length === 32);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function testRoundTrip() {
|
|
58
|
+
for (var i = 0; i < 5; i++) {
|
|
59
|
+
var kp = x.keygen();
|
|
60
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
61
|
+
var ss = x.decapsulate(kp.secretKey, enc.ciphertext);
|
|
62
|
+
check("round-trip " + i + ": decaps recovers the encaps shared secret", ss.equals(enc.sharedSecret));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function testDeterminism() {
|
|
67
|
+
var seed = Buffer.alloc(32, 1);
|
|
68
|
+
check("keygen(seed) is deterministic", x.keygen(seed).publicKey.equals(x.keygen(seed).publicKey));
|
|
69
|
+
var pk = x.keygen(seed).publicKey;
|
|
70
|
+
// Distinct eseed halves so the value depends on the draft's split order
|
|
71
|
+
// (eseed[0:32] = ML-KEM coins, eseed[32:64] = X25519 ephemeral).
|
|
72
|
+
var eseed = Buffer.concat([Buffer.alloc(32, 2), Buffer.alloc(32, 3)]);
|
|
73
|
+
var a = x.encapsulate(pk, eseed), c2 = x.encapsulate(pk, eseed);
|
|
74
|
+
check("encapsulate(pk, eseed) is deterministic", a.ciphertext.equals(c2.ciphertext) && a.sharedSecret.equals(c2.sharedSecret));
|
|
75
|
+
check("deterministic encaps round-trips", x.decapsulate(seed, a.ciphertext).equals(a.sharedSecret));
|
|
76
|
+
|
|
77
|
+
// Regression anchors: pinned digests of the full deterministic flow. A change
|
|
78
|
+
// to the combiner, the seed expansion, the eseed split order, or the wire
|
|
79
|
+
// framing breaks these.
|
|
80
|
+
check("keygen(0x01) public key digest is stable",
|
|
81
|
+
nodeCrypto.createHash("sha3-256").update(pk).digest("hex") === "60068c4c0bfc7421bb1cb4a4202bf0ef75ee27e61bf2f6b08780869485cc736a");
|
|
82
|
+
check("deterministic ciphertext digest is stable",
|
|
83
|
+
nodeCrypto.createHash("sha3-256").update(a.ciphertext).digest("hex") === "1422e82c4307fcb8117b28ceaf686241cbe48d1a5546e05cd1aa7401d5fc2624");
|
|
84
|
+
check("deterministic shared secret is stable",
|
|
85
|
+
a.sharedSecret.toString("hex") === "356f5611bb146e8baf7fa61410552a9c724a170b8a0d4e742fac19161d8fdf4a");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function testImplicitRejection() {
|
|
89
|
+
var kp = x.keygen();
|
|
90
|
+
var enc = x.encapsulate(kp.publicKey);
|
|
91
|
+
// Wrong key: ML-KEM implicit rejection yields a different 32-byte secret, no throw.
|
|
92
|
+
var wrong = x.decapsulate(x.keygen().secretKey, enc.ciphertext);
|
|
93
|
+
check("wrong key yields a 32-byte secret (no throw)", Buffer.isBuffer(wrong) && wrong.length === 32);
|
|
94
|
+
check("wrong key yields a different secret", !wrong.equals(enc.sharedSecret));
|
|
95
|
+
// Tampered ciphertext: still no throw, different secret.
|
|
96
|
+
var bad = Buffer.from(enc.ciphertext); bad[0] ^= 0xff; bad[bad.length - 1] ^= 0xff;
|
|
97
|
+
var ssBad = x.decapsulate(kp.secretKey, bad);
|
|
98
|
+
check("tampered ciphertext yields a different secret without throwing", !ssBad.equals(enc.sharedSecret));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function testErrors() {
|
|
102
|
+
var kp = x.keygen();
|
|
103
|
+
check("keygen rejects a short seed", code(function () { x.keygen(Buffer.alloc(16)); }) === "xwing/bad-seed");
|
|
104
|
+
check("encapsulate rejects a short pubkey", code(function () { x.encapsulate(Buffer.alloc(100)); }) === "xwing/bad-public-key");
|
|
105
|
+
check("encapsulate rejects a short eseed", code(function () { x.encapsulate(kp.publicKey, Buffer.alloc(32)); }) === "xwing/bad-eseed");
|
|
106
|
+
check("decapsulate rejects a short seed", code(function () { x.decapsulate(Buffer.alloc(16), Buffer.alloc(1120)); }) === "xwing/bad-seed");
|
|
107
|
+
check("decapsulate rejects a short ct", code(function () { x.decapsulate(kp.secretKey, Buffer.alloc(100)); }) === "xwing/bad-ciphertext");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function run() {
|
|
111
|
+
testSurface();
|
|
112
|
+
testCombinerKAT();
|
|
113
|
+
testSizes();
|
|
114
|
+
testRoundTrip();
|
|
115
|
+
testDeterminism();
|
|
116
|
+
testImplicitRejection();
|
|
117
|
+
testErrors();
|
|
118
|
+
}
|
|
119
|
+
module.exports = { run: run };
|
|
120
|
+
if (require.main === module) { run().then(function () { console.log("[crypto-xwing] OK — " + helpers.getChecks() + " checks passed"); }, function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/blamejs-shop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"test": "node test/smoke.js",
|
|
9
9
|
"vendor": "bash scripts/vendor-update.sh",
|
|
10
10
|
"sync-assets": "node scripts/sync-r2-assets.js",
|
|
11
|
-
"
|
|
11
|
+
"release": "node scripts/release.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=24.14.1"
|