@cello-protocol/protocol-types 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connection-package.d.ts +362 -0
- package/dist/connection-package.d.ts.map +1 -0
- package/dist/connection-package.js +426 -0
- package/dist/connection-package.js.map +1 -0
- package/dist/connection-request.d.ts +271 -0
- package/dist/connection-request.d.ts.map +1 -0
- package/dist/connection-request.js +91 -0
- package/dist/connection-request.js.map +1 -0
- package/dist/envelope.d.ts +170 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +776 -0
- package/dist/envelope.js.map +1 -0
- package/dist/frost-dkg.d.ts +145 -0
- package/dist/frost-dkg.d.ts.map +1 -0
- package/dist/frost-dkg.js +28 -0
- package/dist/frost-dkg.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/registration.d.ts +155 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +55 -0
- package/dist/registration.js.map +1 -0
- package/dist/session.d.ts +239 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +198 -0
- package/dist/session.js.map +1 -0
- package/dist/structure2.d.ts +75 -0
- package/dist/structure2.d.ts.map +1 -0
- package/dist/structure2.js +138 -0
- package/dist/structure2.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cello-protocol/protocol-types — CELLO-MSG-001 (v0) + CELLO-MSG-003 (v1)
|
|
3
|
+
* Envelope construction, validation, serialization, and deserialization.
|
|
4
|
+
*
|
|
5
|
+
* ──── v0 Pseudocode (CELLO-MSG-001) ────
|
|
6
|
+
*
|
|
7
|
+
* buildEnvelope(content, keyProvider, timestamp):
|
|
8
|
+
* 1. Reject if content.length > MAX_CONTENT_BYTES (1,048,576) → content_too_large
|
|
9
|
+
* 2. Compute content_hash = msgLeafHash(content) [FIPS 180-4: SHA-256(0x00||content)]
|
|
10
|
+
* 3. Fetch sender_pubkey = await keyProvider.getPublicKey()
|
|
11
|
+
* 4. Build TBS positional array: [0, content_hash, sender_pubkey, timestamp]
|
|
12
|
+
* — timestamp encoded as BigInt so cbor-x emits uint64, not float64
|
|
13
|
+
* 5. tbs_bytes = encodeTBS([0, content_hash, sender_pubkey, BigInt(timestamp)])
|
|
14
|
+
* [RFC 8949 §4.2.1: canonical CBOR, Encoder({tagUint8Array:false})]
|
|
15
|
+
* 6. sender_signature = await keyProvider.sign(tbs_bytes) [RFC 8032: Ed25519]
|
|
16
|
+
* 7. Return envelope with all six fields populated
|
|
17
|
+
*
|
|
18
|
+
* validateEnvelope(envelope):
|
|
19
|
+
* 1. Check protocol_version === 0 → unsupported_version
|
|
20
|
+
* 2. Check presence + exact byte lengths of all typed fields:
|
|
21
|
+
* sender_pubkey: 32 bytes → missing_field / invalid_field
|
|
22
|
+
* content_hash: 32 bytes → missing_field / invalid_field
|
|
23
|
+
* sender_signature: 64 bytes → missing_field / invalid_field
|
|
24
|
+
* 3. Check timestamp >= 0 → invalid_field
|
|
25
|
+
* 4. Check content.length <= MAX_CONTENT_BYTES → content_too_large
|
|
26
|
+
* 5. Recompute expected_hash = msgLeafHash(content)
|
|
27
|
+
* Compare byte-by-byte to content_hash → content_hash_mismatch (BEFORE sig check)
|
|
28
|
+
* 6. Rebuild TBS bytes (same as step 5 in build)
|
|
29
|
+
* 7. verify(sender_pubkey, tbs_bytes, sender_signature) → invalid_field('sender_signature')
|
|
30
|
+
* 8. Return ok: true
|
|
31
|
+
*
|
|
32
|
+
* ──── v1 Pseudocode (CELLO-MSG-003) ────
|
|
33
|
+
*
|
|
34
|
+
* buildEnvelopeV1(content, keyProvider, timestamp, session_id, last_seen_seq):
|
|
35
|
+
* 1. Reject if content.length > MAX_CONTENT_BYTES → content_too_large
|
|
36
|
+
* [no hash/sig computed before this check — AC-009]
|
|
37
|
+
* 2. Compute content_hash = msgLeafHash(content) [FIPS 180-4: SHA-256(0x00||content)]
|
|
38
|
+
* Caller-supplied hashes ignored — SI-001
|
|
39
|
+
* 3. Fetch sender_pubkey = await keyProvider.getPublicKey()
|
|
40
|
+
* 4. Build Structure 1 (TBS) positional 6-element array:
|
|
41
|
+
* [1, content_hash, sender_pubkey, session_id, last_seen_seq, timestamp]
|
|
42
|
+
* — protocol_version=1 (CBOR uint)
|
|
43
|
+
* — content_hash: 32-byte CBOR bstr (SHA-256(0x00||content))
|
|
44
|
+
* — sender_pubkey: 32-byte CBOR bstr (Ed25519 public key, RFC 8032)
|
|
45
|
+
* — session_id: 16-byte CBOR bstr
|
|
46
|
+
* — last_seen_seq: CBOR uint (non-negative integer)
|
|
47
|
+
* — timestamp: CBOR uint (Unix ms; BigInt when > 0xFFFFFFFF for minimal encoding
|
|
48
|
+
* per RFC 8949 §4.2.1)
|
|
49
|
+
* 5. tbs_bytes = CBOR_ENC.encode(tbs_array)
|
|
50
|
+
* [RFC 8949 §4.2.1: canonical CBOR, Encoder({tagUint8Array:false})]
|
|
51
|
+
* 6. sender_signature = await keyProvider.sign(tbs_bytes) [RFC 8032: Ed25519]
|
|
52
|
+
* 7. Return MessageEnvelopeV1 with all eight fields populated
|
|
53
|
+
*
|
|
54
|
+
* validateEnvelopeV1(envelope):
|
|
55
|
+
* 1. Check protocol_version === 1 → unsupported_version (AC-003, AC-004)
|
|
56
|
+
* Hard-reject: no v0 fallback, no negotiation (M1 drops v0)
|
|
57
|
+
* 2. Field presence + byte-length checks:
|
|
58
|
+
* sender_pubkey: 32 bytes → missing_field / invalid_field
|
|
59
|
+
* content_hash: 32 bytes → missing_field / invalid_field
|
|
60
|
+
* session_id: 16 bytes → missing_field / invalid_field
|
|
61
|
+
* sender_signature: 64 bytes → missing_field / invalid_field
|
|
62
|
+
* 3. Check last_seen_seq >= 0, integer → invalid_field
|
|
63
|
+
* 4. Check timestamp >= 0, integer → invalid_field
|
|
64
|
+
* 5. Check content.length <= MAX_CONTENT_BYTES → content_too_large
|
|
65
|
+
* 6. Recompute expected_hash = msgLeafHash(content)
|
|
66
|
+
* Compare byte-by-byte to content_hash → content_hash_mismatch (BEFORE sig check)
|
|
67
|
+
* 7. Rebuild Structure 1 TBS bytes (same as step 5 in build)
|
|
68
|
+
* 8. verify(sender_pubkey, tbs_bytes, sender_signature) → invalid_field('sender_signature')
|
|
69
|
+
* 9. Return ok: true
|
|
70
|
+
*
|
|
71
|
+
* extractStructure1(envelope: MessageEnvelopeV1) → Uint8Array:
|
|
72
|
+
* 1. Build positional 6-element array from envelope fields
|
|
73
|
+
* 2. Return CBOR_ENC.encode([1, content_hash, sender_pubkey, session_id, last_seen_seq, timestamp])
|
|
74
|
+
* This is the exact bytes the relay uses to build Structure 2 (MERKLE-002)
|
|
75
|
+
*
|
|
76
|
+
* References:
|
|
77
|
+
* RFC 8949 §4.2.1 — Core Deterministic Encoding Requirements
|
|
78
|
+
* RFC 8032 — Edwards-Curve Digital Signature Algorithm (EdDSA)
|
|
79
|
+
* FIPS 180-4 — SHA-256 (used by msgLeafHash for content_hash computation)
|
|
80
|
+
*/
|
|
81
|
+
import { Encoder } from "cbor-x";
|
|
82
|
+
import { decode as cborDecode } from "cbor-x";
|
|
83
|
+
import { msgLeafHash, verify } from "@cello-protocol/crypto";
|
|
84
|
+
/** Maximum allowed content size: 1 MiB (AC-009, AC-010, AC-011). */
|
|
85
|
+
export const MAX_CONTENT_BYTES = 1_048_576;
|
|
86
|
+
/**
|
|
87
|
+
* Canonical CBOR encoder per RFC 8949 §4.2.1.
|
|
88
|
+
* tagUint8Array: false — encode Uint8Array as CBOR byte strings (major type 2),
|
|
89
|
+
* not as typed-array tags (tag 64). This is the standard wire representation.
|
|
90
|
+
*/
|
|
91
|
+
const CBOR_ENC = new Encoder({ tagUint8Array: false });
|
|
92
|
+
/**
|
|
93
|
+
* Encode the TBS positional array as canonical CBOR.
|
|
94
|
+
*
|
|
95
|
+
* TBS layout: [protocol_version, content_hash, sender_pubkey, timestamp]
|
|
96
|
+
* - protocol_version: CBOR uint (0)
|
|
97
|
+
* - content_hash: CBOR byte string (32 bytes)
|
|
98
|
+
* - sender_pubkey: CBOR byte string (32 bytes)
|
|
99
|
+
* - timestamp: CBOR uint64 (BigInt to force integer encoding, not float64)
|
|
100
|
+
*
|
|
101
|
+
* RFC 8949 §4.2.1: integers use the shortest encoding; byte strings are verbatim.
|
|
102
|
+
* Using BigInt(timestamp) ensures cbor-x emits 0x1b (8-byte uint64) not 0xfb (float64).
|
|
103
|
+
*/
|
|
104
|
+
function encodeTBS(protocolVersion, contentHash, senderPubkey, timestamp) {
|
|
105
|
+
// RFC 8949 §4.2.1: integers must use the shortest encoding.
|
|
106
|
+
// cbor-x encodes JS numbers ≤ 0xFFFFFFFF as 4-byte uint (minimal) but numbers
|
|
107
|
+
// above that threshold as float64 (0xfb…) — not canonical. BigInt always emits
|
|
108
|
+
// 8-byte uint64 regardless of value — non-minimal for small values.
|
|
109
|
+
// Solution: use BigInt only when the value exceeds uint32 range, ensuring
|
|
110
|
+
// shortest encoding across all valid Unix millisecond timestamps.
|
|
111
|
+
const tsEncoded = timestamp > 0xFFFFFFFF ? BigInt(timestamp) : timestamp;
|
|
112
|
+
const tbs = [protocolVersion, contentHash, senderPubkey, tsEncoded];
|
|
113
|
+
return CBOR_ENC.encode(tbs);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build a signed MessageEnvelope.
|
|
117
|
+
*
|
|
118
|
+
* SI-001: content_hash is ALWAYS recomputed; any caller-supplied value is ignored.
|
|
119
|
+
* AC-010: content > 1 MiB → content_too_large, no hash or signature computed.
|
|
120
|
+
*
|
|
121
|
+
* @param content - Raw message bytes
|
|
122
|
+
* @param keyProvider - Signing key abstraction (K_local)
|
|
123
|
+
* @param timestamp - Unix milliseconds (non-negative)
|
|
124
|
+
*/
|
|
125
|
+
export async function buildEnvelope(content, keyProvider, timestamp) {
|
|
126
|
+
// AC-010: reject oversized content before any crypto
|
|
127
|
+
if (content.length > MAX_CONTENT_BYTES) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: {
|
|
131
|
+
reason: "content_too_large",
|
|
132
|
+
message: `content length ${content.length} exceeds maximum ${MAX_CONTENT_BYTES} bytes`,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// SI-001: always recompute — never trust caller-supplied hash
|
|
137
|
+
const content_hash = msgLeafHash(content);
|
|
138
|
+
const sender_pubkey = await keyProvider.getPublicKey();
|
|
139
|
+
// TBS: positional array [protocol_version, content_hash, sender_pubkey, timestamp]
|
|
140
|
+
const tbsBytes = encodeTBS(0, content_hash, sender_pubkey, timestamp);
|
|
141
|
+
// Ed25519 sign per RFC 8032
|
|
142
|
+
const sender_signature = await keyProvider.sign(tbsBytes);
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
envelope: {
|
|
146
|
+
protocol_version: 0,
|
|
147
|
+
sender_pubkey,
|
|
148
|
+
content,
|
|
149
|
+
content_hash,
|
|
150
|
+
timestamp,
|
|
151
|
+
sender_signature,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate a MessageEnvelope.
|
|
157
|
+
*
|
|
158
|
+
* Validation order (fail-fast):
|
|
159
|
+
* 1. protocol_version check (AC-013, SI-004) — before any other check
|
|
160
|
+
* 2. Field presence and byte-length checks (AC-003, AC-004)
|
|
161
|
+
* 3. timestamp range check (AC-005)
|
|
162
|
+
* 4. content size check (AC-011)
|
|
163
|
+
* 5. content_hash recomputation and comparison (AC-012) — BEFORE signature check
|
|
164
|
+
* 6. Signature verification (AC-002, AC-007, SI-003)
|
|
165
|
+
*/
|
|
166
|
+
export function validateEnvelope(envelope) {
|
|
167
|
+
// Step 1: version check — first so unknown versions are detectable without parsing (SI-004)
|
|
168
|
+
if (envelope.protocol_version !== 0) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: {
|
|
172
|
+
reason: "unsupported_version",
|
|
173
|
+
message: `unsupported protocol_version: ${envelope.protocol_version}`,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// Step 2: field presence and size checks
|
|
178
|
+
if (envelope.sender_pubkey === undefined ||
|
|
179
|
+
envelope.sender_pubkey === null) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: { reason: "missing_field", field: "sender_pubkey", message: "sender_pubkey is missing" },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (envelope.sender_pubkey.length !== 32) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
error: {
|
|
189
|
+
reason: "invalid_field",
|
|
190
|
+
field: "sender_pubkey",
|
|
191
|
+
message: `sender_pubkey must be 32 bytes, got ${envelope.sender_pubkey.length}`,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (envelope.content_hash === undefined ||
|
|
196
|
+
envelope.content_hash === null) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
error: { reason: "missing_field", field: "content_hash", message: "content_hash is missing" },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (envelope.content_hash.length !== 32) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: {
|
|
206
|
+
reason: "invalid_field",
|
|
207
|
+
field: "content_hash",
|
|
208
|
+
message: `content_hash must be 32 bytes, got ${envelope.content_hash.length}`,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (envelope.sender_signature === undefined ||
|
|
213
|
+
envelope.sender_signature === null) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: { reason: "missing_field", field: "sender_signature", message: "sender_signature is missing" },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (envelope.sender_signature.length !== 64) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: {
|
|
223
|
+
reason: "invalid_field",
|
|
224
|
+
field: "sender_signature",
|
|
225
|
+
message: `sender_signature must be 64 bytes, got ${envelope.sender_signature.length}`,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Step 3: timestamp range
|
|
230
|
+
if (envelope.timestamp < 0 || !Number.isInteger(envelope.timestamp)) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
error: {
|
|
234
|
+
reason: "invalid_field",
|
|
235
|
+
field: "timestamp",
|
|
236
|
+
message: `timestamp must be a non-negative integer, got ${envelope.timestamp}`,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// Step 4: content size
|
|
241
|
+
if (envelope.content.length > MAX_CONTENT_BYTES) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
error: {
|
|
245
|
+
reason: "content_too_large",
|
|
246
|
+
message: `content length ${envelope.content.length} exceeds maximum ${MAX_CONTENT_BYTES} bytes`,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Step 5: content_hash recomputation — checked BEFORE signature (AC-012)
|
|
251
|
+
const expectedHash = msgLeafHash(envelope.content);
|
|
252
|
+
if (!bytesEqual(expectedHash, envelope.content_hash)) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: {
|
|
256
|
+
reason: "content_hash_mismatch",
|
|
257
|
+
message: "content_hash does not match SHA-256(0x00 || content)",
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Step 6: signature verification
|
|
262
|
+
const tbsBytes = encodeTBS(envelope.protocol_version, envelope.content_hash, envelope.sender_pubkey, envelope.timestamp);
|
|
263
|
+
if (!verify(envelope.sender_pubkey, tbsBytes, envelope.sender_signature)) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: {
|
|
267
|
+
reason: "invalid_field",
|
|
268
|
+
field: "sender_signature",
|
|
269
|
+
message: "signature verification failed",
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return { ok: true };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Serialize a MessageEnvelope to canonical CBOR bytes (RFC 8949 §4.2.1).
|
|
277
|
+
*
|
|
278
|
+
* The envelope is encoded as a CBOR map with string keys.
|
|
279
|
+
* timestamp uses minimal encoding per RFC 8949 §4.2.1 — see encodeTBS comment.
|
|
280
|
+
*/
|
|
281
|
+
export function serializeEnvelope(envelope) {
|
|
282
|
+
const map = {
|
|
283
|
+
protocol_version: envelope.protocol_version,
|
|
284
|
+
sender_pubkey: envelope.sender_pubkey,
|
|
285
|
+
content: envelope.content,
|
|
286
|
+
content_hash: envelope.content_hash,
|
|
287
|
+
timestamp: envelope.timestamp > 0xFFFFFFFF ? BigInt(envelope.timestamp) : envelope.timestamp,
|
|
288
|
+
sender_signature: envelope.sender_signature,
|
|
289
|
+
};
|
|
290
|
+
return CBOR_ENC.encode(map);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Deserialize a MessageEnvelope from CBOR bytes.
|
|
294
|
+
*
|
|
295
|
+
* Performs structural validation only (field presence, types, sizes).
|
|
296
|
+
* Does NOT re-validate the signature or content_hash — call validateEnvelope for that.
|
|
297
|
+
*/
|
|
298
|
+
export function deserializeEnvelope(bytes) {
|
|
299
|
+
let raw;
|
|
300
|
+
try {
|
|
301
|
+
raw = cborDecode(bytes);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: {
|
|
307
|
+
reason: "invalid_field",
|
|
308
|
+
field: "bytes",
|
|
309
|
+
message: `CBOR decode failed: ${e.message}`,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
error: {
|
|
317
|
+
reason: "invalid_field",
|
|
318
|
+
field: "bytes",
|
|
319
|
+
message: "decoded value is not a map",
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const obj = raw;
|
|
324
|
+
// protocol_version
|
|
325
|
+
if (!("protocol_version" in obj)) {
|
|
326
|
+
return { ok: false, error: { reason: "missing_field", field: "protocol_version", message: "protocol_version is missing" } };
|
|
327
|
+
}
|
|
328
|
+
const pv = obj["protocol_version"];
|
|
329
|
+
if (typeof pv !== "number" || pv !== 0) {
|
|
330
|
+
return { ok: false, error: { reason: "unsupported_version", message: `unsupported protocol_version: ${pv}` } };
|
|
331
|
+
}
|
|
332
|
+
// sender_pubkey
|
|
333
|
+
if (!("sender_pubkey" in obj)) {
|
|
334
|
+
return { ok: false, error: { reason: "missing_field", field: "sender_pubkey", message: "sender_pubkey is missing" } };
|
|
335
|
+
}
|
|
336
|
+
const spk = toUint8Array(obj["sender_pubkey"]);
|
|
337
|
+
if (!spk || spk.length !== 32) {
|
|
338
|
+
return { ok: false, error: { reason: "invalid_field", field: "sender_pubkey", message: `sender_pubkey must be 32 bytes` } };
|
|
339
|
+
}
|
|
340
|
+
// content
|
|
341
|
+
if (!("content" in obj)) {
|
|
342
|
+
return { ok: false, error: { reason: "missing_field", field: "content", message: "content is missing" } };
|
|
343
|
+
}
|
|
344
|
+
const content = toUint8Array(obj["content"]);
|
|
345
|
+
if (!content) {
|
|
346
|
+
return { ok: false, error: { reason: "invalid_field", field: "content", message: "content must be bytes" } };
|
|
347
|
+
}
|
|
348
|
+
// content_hash
|
|
349
|
+
if (!("content_hash" in obj)) {
|
|
350
|
+
return { ok: false, error: { reason: "missing_field", field: "content_hash", message: "content_hash is missing" } };
|
|
351
|
+
}
|
|
352
|
+
const ch = toUint8Array(obj["content_hash"]);
|
|
353
|
+
if (!ch || ch.length !== 32) {
|
|
354
|
+
return { ok: false, error: { reason: "invalid_field", field: "content_hash", message: `content_hash must be 32 bytes` } };
|
|
355
|
+
}
|
|
356
|
+
// timestamp
|
|
357
|
+
if (!("timestamp" in obj)) {
|
|
358
|
+
return { ok: false, error: { reason: "missing_field", field: "timestamp", message: "timestamp is missing" } };
|
|
359
|
+
}
|
|
360
|
+
const tsRaw = obj["timestamp"];
|
|
361
|
+
const ts = typeof tsRaw === "bigint" ? Number(tsRaw) : (typeof tsRaw === "number" ? tsRaw : NaN);
|
|
362
|
+
if (!Number.isInteger(ts) || ts < 0 || ts > Number.MAX_SAFE_INTEGER) {
|
|
363
|
+
return { ok: false, error: { reason: "invalid_field", field: "timestamp", message: `invalid timestamp: ${tsRaw}` } };
|
|
364
|
+
}
|
|
365
|
+
// sender_signature
|
|
366
|
+
if (!("sender_signature" in obj)) {
|
|
367
|
+
return { ok: false, error: { reason: "missing_field", field: "sender_signature", message: "sender_signature is missing" } };
|
|
368
|
+
}
|
|
369
|
+
const sig = toUint8Array(obj["sender_signature"]);
|
|
370
|
+
if (!sig || sig.length !== 64) {
|
|
371
|
+
return { ok: false, error: { reason: "invalid_field", field: "sender_signature", message: `sender_signature must be 64 bytes` } };
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
envelope: {
|
|
376
|
+
protocol_version: 0,
|
|
377
|
+
sender_pubkey: spk,
|
|
378
|
+
content,
|
|
379
|
+
content_hash: ch,
|
|
380
|
+
timestamp: ts,
|
|
381
|
+
sender_signature: sig,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// ─── v1 functions (CELLO-MSG-003) ────────────────────────────────────────────
|
|
386
|
+
/**
|
|
387
|
+
* Encode the v1 Structure 1 (TBS) positional 6-element array as canonical CBOR.
|
|
388
|
+
*
|
|
389
|
+
* Structure 1 layout: [protocol_version, content_hash, sender_pubkey, session_id, last_seen_seq, timestamp]
|
|
390
|
+
* - protocol_version: CBOR uint, always 1
|
|
391
|
+
* - content_hash: CBOR byte string (32 bytes, SHA-256(0x00||content))
|
|
392
|
+
* - sender_pubkey: CBOR byte string (32 bytes, Ed25519 public key per RFC 8032)
|
|
393
|
+
* - session_id: CBOR byte string (16 bytes)
|
|
394
|
+
* - last_seen_seq: CBOR uint (non-negative integer)
|
|
395
|
+
* - timestamp: CBOR uint (Unix ms; BigInt for values > 0xFFFFFFFF per RFC 8949 §4.2.1 minimal encoding)
|
|
396
|
+
*
|
|
397
|
+
* RFC 8949 §4.2.1: integers must use shortest encoding. BigInt forces 8-byte uint64 for
|
|
398
|
+
* timestamps above uint32 range (all valid Unix ms timestamps since ~Feb 2106).
|
|
399
|
+
*/
|
|
400
|
+
function encodeTBSV1(contentHash, senderPubkey, sessionId, lastSeenSeq, timestamp) {
|
|
401
|
+
const tsEncoded = timestamp > 0xFFFFFFFF ? BigInt(timestamp) : timestamp;
|
|
402
|
+
const tbs = [1, contentHash, senderPubkey, sessionId, lastSeenSeq, tsEncoded];
|
|
403
|
+
return CBOR_ENC.encode(tbs);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Build a signed v1 MessageEnvelopeV1 (CELLO-MSG-003).
|
|
407
|
+
*
|
|
408
|
+
* SI-001: content_hash is ALWAYS recomputed from content; any caller-supplied value is ignored.
|
|
409
|
+
* AC-009: content > 1 MiB → content_too_large, no hash or signature computed.
|
|
410
|
+
*
|
|
411
|
+
* @param content - Raw message bytes (up to 1 MiB)
|
|
412
|
+
* @param keyProvider - Signing key abstraction (K_local)
|
|
413
|
+
* @param timestamp - Unix milliseconds (non-negative)
|
|
414
|
+
* @param session_id - 16-byte session identifier
|
|
415
|
+
* @param last_seen_seq - Highest canonical seq number seen from relay (0 for first message)
|
|
416
|
+
*/
|
|
417
|
+
export async function buildEnvelopeV1(content, keyProvider, timestamp, session_id, last_seen_seq) {
|
|
418
|
+
// session_id must be exactly 16 bytes — catch at build time, not just validation time
|
|
419
|
+
if (session_id.length !== 16) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: {
|
|
423
|
+
reason: "invalid_field",
|
|
424
|
+
field: "session_id",
|
|
425
|
+
message: `session_id must be 16 bytes, got ${session_id.length}`,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// AC-009: reject oversized content before any crypto
|
|
430
|
+
if (content.length > MAX_CONTENT_BYTES) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
error: {
|
|
434
|
+
reason: "content_too_large",
|
|
435
|
+
message: `content length ${content.length} exceeds maximum ${MAX_CONTENT_BYTES} bytes`,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// SI-001: always recompute — never trust caller-supplied hash
|
|
440
|
+
const content_hash = msgLeafHash(content);
|
|
441
|
+
const sender_pubkey = await keyProvider.getPublicKey();
|
|
442
|
+
// Structure 1 (TBS): [1, content_hash, sender_pubkey, session_id, last_seen_seq, timestamp]
|
|
443
|
+
const tbsBytes = encodeTBSV1(content_hash, sender_pubkey, session_id, last_seen_seq, timestamp);
|
|
444
|
+
// Ed25519 sign per RFC 8032
|
|
445
|
+
const sender_signature = await keyProvider.sign(tbsBytes);
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
envelope: {
|
|
449
|
+
protocol_version: 1,
|
|
450
|
+
sender_pubkey,
|
|
451
|
+
content,
|
|
452
|
+
content_hash,
|
|
453
|
+
session_id,
|
|
454
|
+
last_seen_seq,
|
|
455
|
+
timestamp,
|
|
456
|
+
sender_signature,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Validate a v1 MessageEnvelopeV1 (CELLO-MSG-003).
|
|
462
|
+
*
|
|
463
|
+
* Validation order (fail-fast):
|
|
464
|
+
* 1. protocol_version === 1 check — hard-reject v0 and any other version (AC-003, AC-004)
|
|
465
|
+
* 2. Field presence and byte-length checks
|
|
466
|
+
* 3. last_seen_seq and timestamp range checks
|
|
467
|
+
* 4. content size check
|
|
468
|
+
* 5. content_hash recomputation (BEFORE signature check) (AC-006)
|
|
469
|
+
* 6. Signature verification over Structure 1 TBS
|
|
470
|
+
*/
|
|
471
|
+
export function validateEnvelopeV1(envelope) {
|
|
472
|
+
// Step 1: version gate — hard-reject any version != 1 (M1 drops v0; no negotiation)
|
|
473
|
+
if (envelope.protocol_version !== 1) {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: {
|
|
477
|
+
reason: "unsupported_version",
|
|
478
|
+
message: `unsupported protocol_version: ${envelope.protocol_version}`,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
// Step 2a: sender_pubkey — 32 bytes
|
|
483
|
+
if (envelope.sender_pubkey === undefined || envelope.sender_pubkey === null) {
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
error: { reason: "missing_field", field: "sender_pubkey", message: "sender_pubkey is missing" },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (envelope.sender_pubkey.length !== 32) {
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
error: {
|
|
493
|
+
reason: "invalid_field",
|
|
494
|
+
field: "sender_pubkey",
|
|
495
|
+
message: `sender_pubkey must be 32 bytes, got ${envelope.sender_pubkey.length}`,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
// Step 2b: content_hash — 32 bytes
|
|
500
|
+
if (envelope.content_hash === undefined || envelope.content_hash === null) {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
error: { reason: "missing_field", field: "content_hash", message: "content_hash is missing" },
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (envelope.content_hash.length !== 32) {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
error: {
|
|
510
|
+
reason: "invalid_field",
|
|
511
|
+
field: "content_hash",
|
|
512
|
+
message: `content_hash must be 32 bytes, got ${envelope.content_hash.length}`,
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
// Step 2c: session_id — 16 bytes
|
|
517
|
+
if (envelope.session_id === undefined || envelope.session_id === null) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
error: { reason: "missing_field", field: "session_id", message: "session_id is missing" },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (envelope.session_id.length !== 16) {
|
|
524
|
+
return {
|
|
525
|
+
ok: false,
|
|
526
|
+
error: {
|
|
527
|
+
reason: "invalid_field",
|
|
528
|
+
field: "session_id",
|
|
529
|
+
message: `session_id must be 16 bytes, got ${envelope.session_id.length}`,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// Step 2d: sender_signature — 64 bytes
|
|
534
|
+
if (envelope.sender_signature === undefined || envelope.sender_signature === null) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
error: { reason: "missing_field", field: "sender_signature", message: "sender_signature is missing" },
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (envelope.sender_signature.length !== 64) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
error: {
|
|
544
|
+
reason: "invalid_field",
|
|
545
|
+
field: "sender_signature",
|
|
546
|
+
message: `sender_signature must be 64 bytes, got ${envelope.sender_signature.length}`,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Step 3: last_seen_seq range
|
|
551
|
+
if (!Number.isInteger(envelope.last_seen_seq) || envelope.last_seen_seq < 0) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
error: {
|
|
555
|
+
reason: "invalid_field",
|
|
556
|
+
field: "last_seen_seq",
|
|
557
|
+
message: `last_seen_seq must be a non-negative integer, got ${envelope.last_seen_seq}`,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
// Step 4: timestamp range
|
|
562
|
+
if (!Number.isInteger(envelope.timestamp) || envelope.timestamp < 0) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: {
|
|
566
|
+
reason: "invalid_field",
|
|
567
|
+
field: "timestamp",
|
|
568
|
+
message: `timestamp must be a non-negative integer, got ${envelope.timestamp}`,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// Step 5: content size
|
|
573
|
+
if (envelope.content.length > MAX_CONTENT_BYTES) {
|
|
574
|
+
return {
|
|
575
|
+
ok: false,
|
|
576
|
+
error: {
|
|
577
|
+
reason: "content_too_large",
|
|
578
|
+
message: `content length ${envelope.content.length} exceeds maximum ${MAX_CONTENT_BYTES} bytes`,
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// Step 6: content_hash recomputation — checked BEFORE signature (AC-006)
|
|
583
|
+
const expectedHash = msgLeafHash(envelope.content);
|
|
584
|
+
if (!bytesEqual(expectedHash, envelope.content_hash)) {
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: {
|
|
588
|
+
reason: "content_hash_mismatch",
|
|
589
|
+
message: "content_hash does not match SHA-256(0x00 || content)",
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
// Step 7: signature verification over Structure 1 TBS
|
|
594
|
+
const tbsBytes = encodeTBSV1(envelope.content_hash, envelope.sender_pubkey, envelope.session_id, envelope.last_seen_seq, envelope.timestamp);
|
|
595
|
+
if (!verify(envelope.sender_pubkey, tbsBytes, envelope.sender_signature)) {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
error: {
|
|
599
|
+
reason: "invalid_field",
|
|
600
|
+
field: "sender_signature",
|
|
601
|
+
message: "signature verification failed",
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return { ok: true };
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Serialize a v1 MessageEnvelopeV1 to canonical CBOR bytes (RFC 8949 §4.2.1).
|
|
609
|
+
*
|
|
610
|
+
* The envelope is encoded as a CBOR map with string keys.
|
|
611
|
+
* timestamp uses minimal encoding per RFC 8949 §4.2.1.
|
|
612
|
+
*/
|
|
613
|
+
export function serializeEnvelopeV1(envelope) {
|
|
614
|
+
const map = {
|
|
615
|
+
protocol_version: envelope.protocol_version,
|
|
616
|
+
sender_pubkey: envelope.sender_pubkey,
|
|
617
|
+
content: envelope.content,
|
|
618
|
+
content_hash: envelope.content_hash,
|
|
619
|
+
session_id: envelope.session_id,
|
|
620
|
+
last_seen_seq: envelope.last_seen_seq,
|
|
621
|
+
timestamp: envelope.timestamp > 0xFFFFFFFF ? BigInt(envelope.timestamp) : envelope.timestamp,
|
|
622
|
+
sender_signature: envelope.sender_signature,
|
|
623
|
+
};
|
|
624
|
+
return CBOR_ENC.encode(map);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Deserialize a v1 MessageEnvelopeV1 from CBOR bytes.
|
|
628
|
+
*
|
|
629
|
+
* Performs structural validation only (field presence, types, sizes).
|
|
630
|
+
* Does NOT re-validate the signature or content_hash — call validateEnvelopeV1 for that.
|
|
631
|
+
*/
|
|
632
|
+
export function deserializeEnvelopeV1(bytes) {
|
|
633
|
+
let raw;
|
|
634
|
+
try {
|
|
635
|
+
raw = cborDecode(bytes);
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
error: {
|
|
641
|
+
reason: "invalid_field",
|
|
642
|
+
field: "bytes",
|
|
643
|
+
message: `CBOR decode failed: ${e.message}`,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
648
|
+
return {
|
|
649
|
+
ok: false,
|
|
650
|
+
error: {
|
|
651
|
+
reason: "invalid_field",
|
|
652
|
+
field: "bytes",
|
|
653
|
+
message: "decoded value is not a map",
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const obj = raw;
|
|
658
|
+
// protocol_version — must be exactly 1
|
|
659
|
+
if (!("protocol_version" in obj)) {
|
|
660
|
+
return { ok: false, error: { reason: "missing_field", field: "protocol_version", message: "protocol_version is missing" } };
|
|
661
|
+
}
|
|
662
|
+
const pv = obj["protocol_version"];
|
|
663
|
+
if (typeof pv !== "number" || pv !== 1) {
|
|
664
|
+
return { ok: false, error: { reason: "unsupported_version", message: `unsupported protocol_version: ${pv}` } };
|
|
665
|
+
}
|
|
666
|
+
// sender_pubkey — 32 bytes
|
|
667
|
+
if (!("sender_pubkey" in obj)) {
|
|
668
|
+
return { ok: false, error: { reason: "missing_field", field: "sender_pubkey", message: "sender_pubkey is missing" } };
|
|
669
|
+
}
|
|
670
|
+
const spk = toUint8Array(obj["sender_pubkey"]);
|
|
671
|
+
if (!spk || spk.length !== 32) {
|
|
672
|
+
return { ok: false, error: { reason: "invalid_field", field: "sender_pubkey", message: "sender_pubkey must be 32 bytes" } };
|
|
673
|
+
}
|
|
674
|
+
// content
|
|
675
|
+
if (!("content" in obj)) {
|
|
676
|
+
return { ok: false, error: { reason: "missing_field", field: "content", message: "content is missing" } };
|
|
677
|
+
}
|
|
678
|
+
const content = toUint8Array(obj["content"]);
|
|
679
|
+
if (!content) {
|
|
680
|
+
return { ok: false, error: { reason: "invalid_field", field: "content", message: "content must be bytes" } };
|
|
681
|
+
}
|
|
682
|
+
// content_hash — 32 bytes
|
|
683
|
+
if (!("content_hash" in obj)) {
|
|
684
|
+
return { ok: false, error: { reason: "missing_field", field: "content_hash", message: "content_hash is missing" } };
|
|
685
|
+
}
|
|
686
|
+
const ch = toUint8Array(obj["content_hash"]);
|
|
687
|
+
if (!ch || ch.length !== 32) {
|
|
688
|
+
return { ok: false, error: { reason: "invalid_field", field: "content_hash", message: "content_hash must be 32 bytes" } };
|
|
689
|
+
}
|
|
690
|
+
// session_id — 16 bytes
|
|
691
|
+
if (!("session_id" in obj)) {
|
|
692
|
+
return { ok: false, error: { reason: "missing_field", field: "session_id", message: "session_id is missing" } };
|
|
693
|
+
}
|
|
694
|
+
const sid = toUint8Array(obj["session_id"]);
|
|
695
|
+
if (!sid || sid.length !== 16) {
|
|
696
|
+
return { ok: false, error: { reason: "invalid_field", field: "session_id", message: "session_id must be 16 bytes" } };
|
|
697
|
+
}
|
|
698
|
+
// last_seen_seq — non-negative integer
|
|
699
|
+
if (!("last_seen_seq" in obj)) {
|
|
700
|
+
return { ok: false, error: { reason: "missing_field", field: "last_seen_seq", message: "last_seen_seq is missing" } };
|
|
701
|
+
}
|
|
702
|
+
const lssRaw = obj["last_seen_seq"];
|
|
703
|
+
const lss = typeof lssRaw === "bigint" ? Number(lssRaw) : (typeof lssRaw === "number" ? lssRaw : NaN);
|
|
704
|
+
if (!Number.isInteger(lss) || lss < 0 || lss > Number.MAX_SAFE_INTEGER) {
|
|
705
|
+
return { ok: false, error: { reason: "invalid_field", field: "last_seen_seq", message: `invalid last_seen_seq: ${lssRaw}` } };
|
|
706
|
+
}
|
|
707
|
+
// timestamp — non-negative integer
|
|
708
|
+
if (!("timestamp" in obj)) {
|
|
709
|
+
return { ok: false, error: { reason: "missing_field", field: "timestamp", message: "timestamp is missing" } };
|
|
710
|
+
}
|
|
711
|
+
const tsRaw = obj["timestamp"];
|
|
712
|
+
const ts = typeof tsRaw === "bigint" ? Number(tsRaw) : (typeof tsRaw === "number" ? tsRaw : NaN);
|
|
713
|
+
if (!Number.isInteger(ts) || ts < 0 || ts > Number.MAX_SAFE_INTEGER) {
|
|
714
|
+
return { ok: false, error: { reason: "invalid_field", field: "timestamp", message: `invalid timestamp: ${tsRaw}` } };
|
|
715
|
+
}
|
|
716
|
+
// sender_signature — 64 bytes
|
|
717
|
+
if (!("sender_signature" in obj)) {
|
|
718
|
+
return { ok: false, error: { reason: "missing_field", field: "sender_signature", message: "sender_signature is missing" } };
|
|
719
|
+
}
|
|
720
|
+
const sig = toUint8Array(obj["sender_signature"]);
|
|
721
|
+
if (!sig || sig.length !== 64) {
|
|
722
|
+
return { ok: false, error: { reason: "invalid_field", field: "sender_signature", message: "sender_signature must be 64 bytes" } };
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
ok: true,
|
|
726
|
+
envelope: {
|
|
727
|
+
protocol_version: 1,
|
|
728
|
+
sender_pubkey: spk,
|
|
729
|
+
content,
|
|
730
|
+
content_hash: ch,
|
|
731
|
+
session_id: sid,
|
|
732
|
+
last_seen_seq: lss,
|
|
733
|
+
timestamp: ts,
|
|
734
|
+
sender_signature: sig,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Extract Structure 1 from a v1 envelope — returns the canonical CBOR bytes that were signed.
|
|
740
|
+
*
|
|
741
|
+
* Structure 1: canonical_CBOR([1, content_hash, sender_pubkey, session_id, last_seen_seq, timestamp])
|
|
742
|
+
*
|
|
743
|
+
* This is the exact bytes the relay uses to build Structure 2 (CELLO-MERKLE-002).
|
|
744
|
+
* Per RFC 8949 §4.2.1 canonical CBOR and RFC 8032 Ed25519.
|
|
745
|
+
*/
|
|
746
|
+
export function extractStructure1(envelope) {
|
|
747
|
+
return encodeTBSV1(envelope.content_hash, envelope.sender_pubkey, envelope.session_id, envelope.last_seen_seq, envelope.timestamp);
|
|
748
|
+
}
|
|
749
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
750
|
+
/**
|
|
751
|
+
* Constant-time byte comparison.
|
|
752
|
+
* Note: for HMAC verification, use a real constant-time compare.
|
|
753
|
+
* For hash comparison this is sufficient since timing leaks only reveal
|
|
754
|
+
* content we already possess.
|
|
755
|
+
*/
|
|
756
|
+
function bytesEqual(a, b) {
|
|
757
|
+
if (a.length !== b.length)
|
|
758
|
+
return false;
|
|
759
|
+
let diff = 0;
|
|
760
|
+
for (let i = 0; i < a.length; i++) {
|
|
761
|
+
diff |= a[i] ^ b[i];
|
|
762
|
+
}
|
|
763
|
+
return diff === 0;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Coerce a CBOR-decoded value to Uint8Array.
|
|
767
|
+
* cbor-x with tagUint8Array:false returns Buffer (a Node.js Buffer is a Uint8Array subclass).
|
|
768
|
+
*/
|
|
769
|
+
function toUint8Array(v) {
|
|
770
|
+
if (v instanceof Uint8Array)
|
|
771
|
+
return v;
|
|
772
|
+
if (Buffer.isBuffer(v))
|
|
773
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
//# sourceMappingURL=envelope.js.map
|