@bcts/provenance-mark 1.0.0-alpha.5
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 +48 -0
- package/README.md +15 -0
- package/dist/index.cjs +1703 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +711 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +711 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +1700 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +1657 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +79 -0
- package/src/crypto-utils.ts +75 -0
- package/src/date.ts +216 -0
- package/src/error.ts +141 -0
- package/src/generator.ts +182 -0
- package/src/index.ts +90 -0
- package/src/mark-info.ts +126 -0
- package/src/mark.ts +597 -0
- package/src/resolution.ts +294 -0
- package/src/rng-state.ts +68 -0
- package/src/seed.ts +98 -0
- package/src/utils.ts +103 -0
- package/src/validate.ts +449 -0
- package/src/xoshiro256starstar.ts +150 -0
package/src/mark.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// Ported from provenance-mark-rust/src/mark.rs
|
|
2
|
+
|
|
3
|
+
import { toBase64, fromBase64, bytesToHex } from "./utils.js";
|
|
4
|
+
import { type Cbor, cbor, cborData, expectArray, expectBytes, decodeCbor } from "@bcts/dcbor";
|
|
5
|
+
import { PROVENANCE_MARK } from "@bcts/tags";
|
|
6
|
+
import {
|
|
7
|
+
BytewordsStyle,
|
|
8
|
+
encodeBytewords,
|
|
9
|
+
decodeBytewords,
|
|
10
|
+
encodeBytewordsIdentifier,
|
|
11
|
+
encodeBytemojisIdentifier,
|
|
12
|
+
} from "@bcts/uniform-resources";
|
|
13
|
+
|
|
14
|
+
import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
|
|
15
|
+
import {
|
|
16
|
+
type ProvenanceMarkResolution,
|
|
17
|
+
linkLength,
|
|
18
|
+
keyRange,
|
|
19
|
+
chainIdRange,
|
|
20
|
+
hashRange,
|
|
21
|
+
seqBytesRange,
|
|
22
|
+
dateBytesRange,
|
|
23
|
+
infoRangeStart,
|
|
24
|
+
fixedLength,
|
|
25
|
+
serializeDate,
|
|
26
|
+
deserializeDate,
|
|
27
|
+
serializeSeq,
|
|
28
|
+
deserializeSeq,
|
|
29
|
+
resolutionFromCbor,
|
|
30
|
+
resolutionToCbor,
|
|
31
|
+
} from "./resolution.js";
|
|
32
|
+
import { sha256, sha256Prefix, obfuscate } from "./crypto-utils.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A cryptographically-secured provenance mark.
|
|
36
|
+
*/
|
|
37
|
+
export class ProvenanceMark {
|
|
38
|
+
private readonly _res: ProvenanceMarkResolution;
|
|
39
|
+
private readonly _key: Uint8Array;
|
|
40
|
+
private readonly _hash: Uint8Array;
|
|
41
|
+
private readonly _chainId: Uint8Array;
|
|
42
|
+
private readonly _seqBytes: Uint8Array;
|
|
43
|
+
private readonly _dateBytes: Uint8Array;
|
|
44
|
+
private readonly _infoBytes: Uint8Array;
|
|
45
|
+
private readonly _seq: number;
|
|
46
|
+
private readonly _date: Date;
|
|
47
|
+
|
|
48
|
+
private constructor(
|
|
49
|
+
res: ProvenanceMarkResolution,
|
|
50
|
+
key: Uint8Array,
|
|
51
|
+
hash: Uint8Array,
|
|
52
|
+
chainId: Uint8Array,
|
|
53
|
+
seqBytes: Uint8Array,
|
|
54
|
+
dateBytes: Uint8Array,
|
|
55
|
+
infoBytes: Uint8Array,
|
|
56
|
+
seq: number,
|
|
57
|
+
date: Date,
|
|
58
|
+
) {
|
|
59
|
+
this._res = res;
|
|
60
|
+
this._key = key;
|
|
61
|
+
this._hash = hash;
|
|
62
|
+
this._chainId = chainId;
|
|
63
|
+
this._seqBytes = seqBytes;
|
|
64
|
+
this._dateBytes = dateBytes;
|
|
65
|
+
this._infoBytes = infoBytes;
|
|
66
|
+
this._seq = seq;
|
|
67
|
+
this._date = date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
res(): ProvenanceMarkResolution {
|
|
71
|
+
return this._res;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
key(): Uint8Array {
|
|
75
|
+
return new Uint8Array(this._key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hash(): Uint8Array {
|
|
79
|
+
return new Uint8Array(this._hash);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
chainId(): Uint8Array {
|
|
83
|
+
return new Uint8Array(this._chainId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
seqBytes(): Uint8Array {
|
|
87
|
+
return new Uint8Array(this._seqBytes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
dateBytes(): Uint8Array {
|
|
91
|
+
return new Uint8Array(this._dateBytes);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
seq(): number {
|
|
95
|
+
return this._seq;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
date(): Date {
|
|
99
|
+
return this._date;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the message (serialized bytes) of this mark.
|
|
104
|
+
*/
|
|
105
|
+
message(): Uint8Array {
|
|
106
|
+
const payload = new Uint8Array([
|
|
107
|
+
...this._chainId,
|
|
108
|
+
...this._hash,
|
|
109
|
+
...this._seqBytes,
|
|
110
|
+
...this._dateBytes,
|
|
111
|
+
...this._infoBytes,
|
|
112
|
+
]);
|
|
113
|
+
const obfuscated = obfuscate(this._key, payload);
|
|
114
|
+
return new Uint8Array([...this._key, ...obfuscated]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the info field as CBOR, if present.
|
|
119
|
+
*/
|
|
120
|
+
info(): Cbor | undefined {
|
|
121
|
+
if (this._infoBytes.length === 0) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return decodeCbor(this._infoBytes);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a new provenance mark.
|
|
129
|
+
*/
|
|
130
|
+
static new(
|
|
131
|
+
res: ProvenanceMarkResolution,
|
|
132
|
+
key: Uint8Array,
|
|
133
|
+
nextKey: Uint8Array,
|
|
134
|
+
chainId: Uint8Array,
|
|
135
|
+
seq: number,
|
|
136
|
+
date: Date,
|
|
137
|
+
info?: Cbor,
|
|
138
|
+
): ProvenanceMark {
|
|
139
|
+
const linkLen = linkLength(res);
|
|
140
|
+
|
|
141
|
+
if (key.length !== linkLen) {
|
|
142
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidKeyLength, undefined, {
|
|
143
|
+
expected: linkLen,
|
|
144
|
+
actual: key.length,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (nextKey.length !== linkLen) {
|
|
148
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidNextKeyLength, undefined, {
|
|
149
|
+
expected: linkLen,
|
|
150
|
+
actual: nextKey.length,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (chainId.length !== linkLen) {
|
|
154
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidChainIdLength, undefined, {
|
|
155
|
+
expected: linkLen,
|
|
156
|
+
actual: chainId.length,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const dateBytes = serializeDate(res, date);
|
|
161
|
+
const seqBytes = serializeSeq(res, seq);
|
|
162
|
+
|
|
163
|
+
// Re-deserialize to get normalized date
|
|
164
|
+
const normalizedDate = deserializeDate(res, dateBytes);
|
|
165
|
+
|
|
166
|
+
const infoBytes = info !== undefined ? cborData(info) : new Uint8Array(0);
|
|
167
|
+
|
|
168
|
+
const hash = ProvenanceMark.makeHash(
|
|
169
|
+
res,
|
|
170
|
+
key,
|
|
171
|
+
nextKey,
|
|
172
|
+
chainId,
|
|
173
|
+
seqBytes,
|
|
174
|
+
dateBytes,
|
|
175
|
+
infoBytes,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return new ProvenanceMark(
|
|
179
|
+
res,
|
|
180
|
+
new Uint8Array(key),
|
|
181
|
+
hash,
|
|
182
|
+
new Uint8Array(chainId),
|
|
183
|
+
seqBytes,
|
|
184
|
+
dateBytes,
|
|
185
|
+
infoBytes,
|
|
186
|
+
seq,
|
|
187
|
+
normalizedDate,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a provenance mark from a serialized message.
|
|
193
|
+
*/
|
|
194
|
+
static fromMessage(res: ProvenanceMarkResolution, message: Uint8Array): ProvenanceMark {
|
|
195
|
+
const minLen = fixedLength(res);
|
|
196
|
+
if (message.length < minLen) {
|
|
197
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidMessageLength, undefined, {
|
|
198
|
+
expected: minLen,
|
|
199
|
+
actual: message.length,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const linkLen = linkLength(res);
|
|
204
|
+
const keyRng = keyRange(res);
|
|
205
|
+
const key = message.slice(keyRng.start, keyRng.end);
|
|
206
|
+
|
|
207
|
+
const payload = obfuscate(key, message.slice(linkLen));
|
|
208
|
+
|
|
209
|
+
// All ranges are for the payload, not the message
|
|
210
|
+
const chainIdRng = chainIdRange(res);
|
|
211
|
+
const chainId = payload.slice(chainIdRng.start, chainIdRng.end);
|
|
212
|
+
|
|
213
|
+
const hashRng = hashRange(res);
|
|
214
|
+
const hash = payload.slice(hashRng.start, hashRng.end);
|
|
215
|
+
|
|
216
|
+
const seqRng = seqBytesRange(res);
|
|
217
|
+
const seqBytes = payload.slice(seqRng.start, seqRng.end);
|
|
218
|
+
const seq = deserializeSeq(res, seqBytes);
|
|
219
|
+
|
|
220
|
+
const dateRng = dateBytesRange(res);
|
|
221
|
+
const dateBytes = payload.slice(dateRng.start, dateRng.end);
|
|
222
|
+
const date = deserializeDate(res, dateBytes);
|
|
223
|
+
|
|
224
|
+
const infoStart = infoRangeStart(res);
|
|
225
|
+
const infoBytes = payload.slice(infoStart);
|
|
226
|
+
|
|
227
|
+
// Validate info CBOR if present
|
|
228
|
+
if (infoBytes.length > 0) {
|
|
229
|
+
try {
|
|
230
|
+
decodeCbor(infoBytes);
|
|
231
|
+
} catch {
|
|
232
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidInfoCbor);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return new ProvenanceMark(
|
|
237
|
+
res,
|
|
238
|
+
new Uint8Array(key),
|
|
239
|
+
new Uint8Array(hash),
|
|
240
|
+
new Uint8Array(chainId),
|
|
241
|
+
new Uint8Array(seqBytes),
|
|
242
|
+
new Uint8Array(dateBytes),
|
|
243
|
+
new Uint8Array(infoBytes),
|
|
244
|
+
seq,
|
|
245
|
+
date,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static makeHash(
|
|
250
|
+
res: ProvenanceMarkResolution,
|
|
251
|
+
key: Uint8Array,
|
|
252
|
+
nextKey: Uint8Array,
|
|
253
|
+
chainId: Uint8Array,
|
|
254
|
+
seqBytes: Uint8Array,
|
|
255
|
+
dateBytes: Uint8Array,
|
|
256
|
+
infoBytes: Uint8Array,
|
|
257
|
+
): Uint8Array {
|
|
258
|
+
const buf = new Uint8Array([
|
|
259
|
+
...key,
|
|
260
|
+
...nextKey,
|
|
261
|
+
...chainId,
|
|
262
|
+
...seqBytes,
|
|
263
|
+
...dateBytes,
|
|
264
|
+
...infoBytes,
|
|
265
|
+
]);
|
|
266
|
+
return sha256Prefix(buf, linkLength(res));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get the first four bytes of the hash as a hex string identifier.
|
|
271
|
+
*/
|
|
272
|
+
identifier(): string {
|
|
273
|
+
return Array.from(this._hash.slice(0, 4))
|
|
274
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
275
|
+
.join("");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get the first four bytes of the hash as upper-case ByteWords.
|
|
280
|
+
*/
|
|
281
|
+
bytewordsIdentifier(prefix: boolean): string {
|
|
282
|
+
const bytes = this._hash.slice(0, 4);
|
|
283
|
+
const s = encodeBytewordsIdentifier(bytes).toUpperCase();
|
|
284
|
+
return prefix ? `\u{1F151} ${s}` : s;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the first four bytes of the hash as Bytemoji.
|
|
289
|
+
*/
|
|
290
|
+
bytemojiIdentifier(prefix: boolean): string {
|
|
291
|
+
const bytes = this._hash.slice(0, 4);
|
|
292
|
+
const s = encodeBytemojisIdentifier(bytes).toUpperCase();
|
|
293
|
+
return prefix ? `\u{1F151} ${s}` : s;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if this mark precedes another mark in the chain.
|
|
298
|
+
*/
|
|
299
|
+
precedes(next: ProvenanceMark): boolean {
|
|
300
|
+
try {
|
|
301
|
+
this.precedesOpt(next);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if this mark precedes another mark, throwing on validation errors.
|
|
310
|
+
*/
|
|
311
|
+
precedesOpt(next: ProvenanceMark): void {
|
|
312
|
+
// `next` can't be a genesis
|
|
313
|
+
if (next._seq === 0) {
|
|
314
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, undefined, {
|
|
315
|
+
message: "non-genesis mark at sequence 0",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (arraysEqual(next._key, next._chainId)) {
|
|
319
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, undefined, {
|
|
320
|
+
message: "genesis mark must have key equal to chain_id",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// `next` must have the next highest sequence number
|
|
324
|
+
if (this._seq !== next._seq - 1) {
|
|
325
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, undefined, {
|
|
326
|
+
message: `sequence gap: expected ${this._seq + 1}, got ${next._seq}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// `next` must have an equal or later date
|
|
330
|
+
if (this._date > next._date) {
|
|
331
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, undefined, {
|
|
332
|
+
message: `date ordering: ${this._date.toISOString()} > ${next._date.toISOString()}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// `next` must reveal the key that was used to generate this mark's hash
|
|
336
|
+
const expectedHash = ProvenanceMark.makeHash(
|
|
337
|
+
this._res,
|
|
338
|
+
this._key,
|
|
339
|
+
next._key,
|
|
340
|
+
this._chainId,
|
|
341
|
+
this._seqBytes,
|
|
342
|
+
this._dateBytes,
|
|
343
|
+
this._infoBytes,
|
|
344
|
+
);
|
|
345
|
+
if (!arraysEqual(this._hash, expectedHash)) {
|
|
346
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, undefined, {
|
|
347
|
+
message: "hash mismatch",
|
|
348
|
+
expected: Array.from(expectedHash)
|
|
349
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
350
|
+
.join(""),
|
|
351
|
+
actual: Array.from(this._hash)
|
|
352
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
353
|
+
.join(""),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if a sequence of marks is valid.
|
|
360
|
+
*/
|
|
361
|
+
static isSequenceValid(marks: ProvenanceMark[]): boolean {
|
|
362
|
+
if (marks.length < 2) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (marks[0]._seq === 0 && !marks[0].isGenesis()) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
for (let i = 0; i < marks.length - 1; i++) {
|
|
369
|
+
if (!marks[i].precedes(marks[i + 1])) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if this is a genesis mark (seq 0 and key equals chain_id).
|
|
378
|
+
*/
|
|
379
|
+
isGenesis(): boolean {
|
|
380
|
+
return this._seq === 0 && arraysEqual(this._key, this._chainId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Encode as bytewords with the given style.
|
|
385
|
+
*/
|
|
386
|
+
toBytewordsWithStyle(style: BytewordsStyle): string {
|
|
387
|
+
return encodeBytewords(this.message(), style);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Encode as standard bytewords.
|
|
392
|
+
*/
|
|
393
|
+
toBytewords(): string {
|
|
394
|
+
return this.toBytewordsWithStyle(BytewordsStyle.Standard);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Decode from bytewords.
|
|
399
|
+
*/
|
|
400
|
+
static fromBytewords(res: ProvenanceMarkResolution, bytewords: string): ProvenanceMark {
|
|
401
|
+
const message = decodeBytewords(bytewords, BytewordsStyle.Standard);
|
|
402
|
+
return ProvenanceMark.fromMessage(res, message);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Encode for URL (minimal bytewords of CBOR).
|
|
407
|
+
*/
|
|
408
|
+
toUrlEncoding(): string {
|
|
409
|
+
return encodeBytewords(this.toCborData(), BytewordsStyle.Minimal);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Decode from URL encoding.
|
|
414
|
+
*/
|
|
415
|
+
static fromUrlEncoding(urlEncoding: string): ProvenanceMark {
|
|
416
|
+
const cborData = decodeBytewords(urlEncoding, BytewordsStyle.Minimal);
|
|
417
|
+
const cborValue = decodeCbor(cborData);
|
|
418
|
+
return ProvenanceMark.fromTaggedCbor(cborValue);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Build a URL with this mark as a query parameter.
|
|
423
|
+
*/
|
|
424
|
+
toUrl(base: string): URL {
|
|
425
|
+
const url = new URL(base);
|
|
426
|
+
url.searchParams.set("provenance", this.toUrlEncoding());
|
|
427
|
+
return url;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parse a provenance mark from a URL.
|
|
432
|
+
*/
|
|
433
|
+
static fromUrl(url: URL): ProvenanceMark {
|
|
434
|
+
const param = url.searchParams.get("provenance");
|
|
435
|
+
if (param === null || param === "") {
|
|
436
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.MissingUrlParameter, undefined, {
|
|
437
|
+
parameter: "provenance",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return ProvenanceMark.fromUrlEncoding(param);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get the untagged CBOR representation.
|
|
445
|
+
*/
|
|
446
|
+
untaggedCbor(): Cbor {
|
|
447
|
+
return cbor([resolutionToCbor(this._res), cbor(this.message())]);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get the tagged CBOR representation.
|
|
452
|
+
*/
|
|
453
|
+
taggedCbor(): Cbor {
|
|
454
|
+
return cbor({ tag: PROVENANCE_MARK.value, value: this.untaggedCbor() });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Serialize to CBOR bytes (tagged).
|
|
459
|
+
*/
|
|
460
|
+
toCborData(): Uint8Array {
|
|
461
|
+
return cborData(this.taggedCbor());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create from untagged CBOR.
|
|
466
|
+
*/
|
|
467
|
+
static fromUntaggedCbor(cborValue: Cbor): ProvenanceMark {
|
|
468
|
+
const arr = expectArray(cborValue);
|
|
469
|
+
if (arr.length !== 2) {
|
|
470
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
471
|
+
message: "Invalid provenance mark length",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const res = resolutionFromCbor(arr[0]);
|
|
475
|
+
const message = expectBytes(arr[1]);
|
|
476
|
+
return ProvenanceMark.fromMessage(res, message);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create from tagged CBOR.
|
|
481
|
+
*/
|
|
482
|
+
static fromTaggedCbor(cborValue: Cbor): ProvenanceMark {
|
|
483
|
+
const cborObj = cborValue as { tag?: number; value?: Cbor };
|
|
484
|
+
if (cborObj.tag !== PROVENANCE_MARK.value) {
|
|
485
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
486
|
+
message: `Expected tag ${PROVENANCE_MARK.value}, got ${String(cborObj.tag)}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (cborObj.value === undefined) {
|
|
490
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
491
|
+
message: "Tagged CBOR value is missing",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return ProvenanceMark.fromUntaggedCbor(cborObj.value);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create from CBOR bytes.
|
|
499
|
+
*/
|
|
500
|
+
static fromCborData(data: Uint8Array): ProvenanceMark {
|
|
501
|
+
const cborValue = decodeCbor(data);
|
|
502
|
+
return ProvenanceMark.fromTaggedCbor(cborValue);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Get the fingerprint (SHA-256 of CBOR data).
|
|
507
|
+
*/
|
|
508
|
+
fingerprint(): Uint8Array {
|
|
509
|
+
return sha256(this.toCborData());
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Debug string representation.
|
|
514
|
+
*/
|
|
515
|
+
toString(): string {
|
|
516
|
+
return `ProvenanceMark(${this.identifier()})`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Detailed debug representation.
|
|
521
|
+
*/
|
|
522
|
+
toDebugString(): string {
|
|
523
|
+
const components = [
|
|
524
|
+
`key: ${bytesToHex(this._key)}`,
|
|
525
|
+
`hash: ${bytesToHex(this._hash)}`,
|
|
526
|
+
`chainID: ${bytesToHex(this._chainId)}`,
|
|
527
|
+
`seq: ${this._seq}`,
|
|
528
|
+
`date: ${this._date.toISOString()}`,
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
const info = this.info();
|
|
532
|
+
if (info !== undefined) {
|
|
533
|
+
components.push(`info: ${JSON.stringify(info)}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return `ProvenanceMark(${components.join(", ")})`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Check equality with another mark.
|
|
541
|
+
*/
|
|
542
|
+
equals(other: ProvenanceMark): boolean {
|
|
543
|
+
return this._res === other._res && arraysEqual(this.message(), other.message());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* JSON serialization.
|
|
548
|
+
*/
|
|
549
|
+
toJSON(): Record<string, unknown> {
|
|
550
|
+
const result: Record<string, unknown> = {
|
|
551
|
+
res: this._res,
|
|
552
|
+
key: toBase64(this._key),
|
|
553
|
+
hash: toBase64(this._hash),
|
|
554
|
+
chainID: toBase64(this._chainId),
|
|
555
|
+
seq: this._seq,
|
|
556
|
+
date: this._date.toISOString(),
|
|
557
|
+
};
|
|
558
|
+
if (this._infoBytes.length > 0) {
|
|
559
|
+
result["info_bytes"] = toBase64(this._infoBytes);
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Create from JSON object.
|
|
566
|
+
*/
|
|
567
|
+
static fromJSON(json: Record<string, unknown>): ProvenanceMark {
|
|
568
|
+
const res = json["res"] as ProvenanceMarkResolution;
|
|
569
|
+
const key = fromBase64(json["key"] as string);
|
|
570
|
+
const hash = fromBase64(json["hash"] as string);
|
|
571
|
+
const chainId = fromBase64(json["chainID"] as string);
|
|
572
|
+
const seq = json["seq"] as number;
|
|
573
|
+
const dateStr = json["date"] as string;
|
|
574
|
+
const date = new Date(dateStr);
|
|
575
|
+
|
|
576
|
+
const seqBytes = serializeSeq(res, seq);
|
|
577
|
+
const dateBytes = serializeDate(res, date);
|
|
578
|
+
|
|
579
|
+
let infoBytes: Uint8Array = new Uint8Array(0);
|
|
580
|
+
if (typeof json["info_bytes"] === "string") {
|
|
581
|
+
infoBytes = fromBase64(json["info_bytes"]);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return new ProvenanceMark(res, key, hash, chainId, seqBytes, dateBytes, infoBytes, seq, date);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Helper function to compare two Uint8Arrays.
|
|
590
|
+
*/
|
|
591
|
+
function arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
592
|
+
if (a.length !== b.length) return false;
|
|
593
|
+
for (let i = 0; i < a.length; i++) {
|
|
594
|
+
if (a[i] !== b[i]) return false;
|
|
595
|
+
}
|
|
596
|
+
return true;
|
|
597
|
+
}
|