space-data-module-sdk 0.4.1 → 0.5.4
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/README.md +17 -9
- package/package.json +12 -4
- package/src/browser.js +8 -0
- package/src/bundle/wasm.js +21 -4
- package/src/capabilities.js +46 -0
- package/src/compiler/compileModule.js +79 -13
- package/src/compliance/index.js +4 -2
- package/src/compliance/pluginCompliance.js +4 -47
- package/src/index.d.ts +159 -5
- package/src/index.js +1 -0
- package/src/manifest/browser.js +15 -0
- package/src/transport/index.js +19 -0
- package/src/transport/pki.js +125 -4
- package/src/transport/records.js +836 -0
- package/src/utils/wasmCrypto.js +43 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import * as flatbuffers from "flatbuffers";
|
|
2
|
+
|
|
3
|
+
import { ENC, ENCT, KDF, KeyExchange, SymmetricAlgo } from "spacedatastandards.org/lib/js/ENC/main.js";
|
|
4
|
+
import { PNM, PNMT } from "spacedatastandards.org/lib/js/PNM/main.js";
|
|
5
|
+
import { REC, RECT } from "spacedatastandards.org/lib/js/REC/REC.js";
|
|
6
|
+
import { Record, RecordT } from "spacedatastandards.org/lib/js/REC/Record.js";
|
|
7
|
+
import { RecordType } from "spacedatastandards.org/lib/js/REC/RecordType.js";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
base64ToBytes,
|
|
11
|
+
bytesToBase64,
|
|
12
|
+
bytesToHex,
|
|
13
|
+
toUint8Array,
|
|
14
|
+
} from "../utils/encoding.js";
|
|
15
|
+
import { sha256Bytes } from "../utils/wasmCrypto.js";
|
|
16
|
+
|
|
17
|
+
const TRAILER_MAGIC_TEXT = "$REC";
|
|
18
|
+
const TRAILER_MAGIC_BYTES = new TextEncoder().encode(TRAILER_MAGIC_TEXT);
|
|
19
|
+
const TRAILER_FOOTER_LENGTH = 8;
|
|
20
|
+
const DEFAULT_RECORD_COLLECTION_VERSION = "1.0.0";
|
|
21
|
+
const KEY_EXCHANGE_BY_NAME = Object.freeze({
|
|
22
|
+
X25519: KeyExchange.X25519,
|
|
23
|
+
SECP256K1: KeyExchange.Secp256k1,
|
|
24
|
+
P256: KeyExchange.P256,
|
|
25
|
+
});
|
|
26
|
+
const KEY_EXCHANGE_NAME_BY_VALUE = Object.freeze(
|
|
27
|
+
Object.fromEntries(
|
|
28
|
+
Object.entries(KEY_EXCHANGE_BY_NAME).map(([name, value]) => [value, name]),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
const SYMMETRIC_ALGO_BY_NAME = Object.freeze({
|
|
32
|
+
AES_256_CTR: SymmetricAlgo.AES_256_CTR,
|
|
33
|
+
});
|
|
34
|
+
const SYMMETRIC_ALGO_NAME_BY_VALUE = Object.freeze(
|
|
35
|
+
Object.fromEntries(
|
|
36
|
+
Object.entries(SYMMETRIC_ALGO_BY_NAME).map(([name, value]) => [value, name]),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
const KDF_BY_NAME = Object.freeze({
|
|
40
|
+
HKDF_SHA256: KDF.HKDF_SHA256,
|
|
41
|
+
});
|
|
42
|
+
const KDF_NAME_BY_VALUE = Object.freeze(
|
|
43
|
+
Object.fromEntries(
|
|
44
|
+
Object.entries(KDF_BY_NAME).map(([name, value]) => [value, name]),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const RECORD_TYPE_BY_STANDARD = Object.freeze({
|
|
48
|
+
ENC: RecordType.ENC,
|
|
49
|
+
PNM: RecordType.PNM,
|
|
50
|
+
});
|
|
51
|
+
const STANDARD_BY_RECORD_TYPE = Object.freeze(
|
|
52
|
+
Object.fromEntries(
|
|
53
|
+
Object.entries(RECORD_TYPE_BY_STANDARD).map(([standard, value]) => [
|
|
54
|
+
value,
|
|
55
|
+
standard,
|
|
56
|
+
]),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
const textEncoder = new TextEncoder();
|
|
60
|
+
|
|
61
|
+
function assertBounds(buffer, offset, length, label) {
|
|
62
|
+
if (
|
|
63
|
+
!Number.isSafeInteger(offset) ||
|
|
64
|
+
!Number.isSafeInteger(length) ||
|
|
65
|
+
offset < 0 ||
|
|
66
|
+
length < 0 ||
|
|
67
|
+
offset + length > buffer.length
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(`${label} is out of bounds.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readUint16LE(buffer, offset, label) {
|
|
74
|
+
assertBounds(buffer, offset, 2, label);
|
|
75
|
+
return buffer[offset] | (buffer[offset + 1] << 8);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readUint32LE(buffer, offset, label) {
|
|
79
|
+
assertBounds(buffer, offset, 4, label);
|
|
80
|
+
return (
|
|
81
|
+
buffer[offset] |
|
|
82
|
+
(buffer[offset + 1] << 8) |
|
|
83
|
+
(buffer[offset + 2] << 16) |
|
|
84
|
+
(buffer[offset + 3] << 24)
|
|
85
|
+
) >>> 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readInt32LE(buffer, offset, label) {
|
|
89
|
+
return readUint32LE(buffer, offset, label) | 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readTableFieldOffset(buffer, tableMeta, vtableFieldOffset) {
|
|
93
|
+
const fieldEntryOffset = tableMeta.vtableStart + vtableFieldOffset;
|
|
94
|
+
if (fieldEntryOffset + 2 > tableMeta.vtableEnd) {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
return readUint16LE(buffer, fieldEntryOffset, `${tableMeta.label} field offset`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveRelativeOffset(buffer, offset, label) {
|
|
101
|
+
const relativeOffset = readInt32LE(buffer, offset, label);
|
|
102
|
+
const target = offset + relativeOffset;
|
|
103
|
+
if (!Number.isSafeInteger(target) || target < 0 || target > buffer.length - 4) {
|
|
104
|
+
throw new Error(`${label} points outside the FlatBuffer.`);
|
|
105
|
+
}
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertFlatbufferIdentifier(buffer, identifier, label) {
|
|
110
|
+
if (identifier.length !== flatbuffers.FILE_IDENTIFIER_LENGTH) {
|
|
111
|
+
throw new Error(`FlatBuffer identifier "${identifier}" must be 4 bytes.`);
|
|
112
|
+
}
|
|
113
|
+
assertBounds(
|
|
114
|
+
buffer,
|
|
115
|
+
flatbuffers.SIZEOF_INT,
|
|
116
|
+
flatbuffers.FILE_IDENTIFIER_LENGTH,
|
|
117
|
+
`${label} identifier`,
|
|
118
|
+
);
|
|
119
|
+
for (let index = 0; index < identifier.length; index += 1) {
|
|
120
|
+
if (buffer[flatbuffers.SIZEOF_INT + index] !== identifier.charCodeAt(index)) {
|
|
121
|
+
throw new Error(`${label} is missing the ${identifier} file identifier.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertFlatbufferTable(buffer, tableStart, label) {
|
|
127
|
+
assertBounds(buffer, tableStart, 4, `${label} table header`);
|
|
128
|
+
const vtableDistance = readInt32LE(buffer, tableStart, `${label} vtable offset`);
|
|
129
|
+
const vtableStart = tableStart - vtableDistance;
|
|
130
|
+
if (
|
|
131
|
+
!Number.isSafeInteger(vtableStart) ||
|
|
132
|
+
vtableStart < 0 ||
|
|
133
|
+
vtableStart > buffer.length - 4
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(`${label} vtable offset is invalid.`);
|
|
136
|
+
}
|
|
137
|
+
const vtableLength = readUint16LE(buffer, vtableStart, `${label} vtable length`);
|
|
138
|
+
const objectLength = readUint16LE(
|
|
139
|
+
buffer,
|
|
140
|
+
vtableStart + 2,
|
|
141
|
+
`${label} object length`,
|
|
142
|
+
);
|
|
143
|
+
if (vtableLength < 4 || (vtableLength & 1) !== 0) {
|
|
144
|
+
throw new Error(`${label} vtable length is invalid.`);
|
|
145
|
+
}
|
|
146
|
+
if (objectLength < 4) {
|
|
147
|
+
throw new Error(`${label} object length is invalid.`);
|
|
148
|
+
}
|
|
149
|
+
assertBounds(buffer, vtableStart, vtableLength, `${label} vtable`);
|
|
150
|
+
assertBounds(buffer, tableStart, objectLength, `${label} object`);
|
|
151
|
+
for (let entryOffset = vtableStart + 4; entryOffset < vtableStart + vtableLength; entryOffset += 2) {
|
|
152
|
+
const fieldOffset = readUint16LE(buffer, entryOffset, `${label} field entry`);
|
|
153
|
+
if (fieldOffset !== 0 && (fieldOffset < 4 || fieldOffset >= objectLength)) {
|
|
154
|
+
throw new Error(`${label} field offset is invalid.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
label,
|
|
159
|
+
tableStart,
|
|
160
|
+
tableEnd: tableStart + objectLength,
|
|
161
|
+
objectLength,
|
|
162
|
+
vtableStart,
|
|
163
|
+
vtableEnd: vtableStart + vtableLength,
|
|
164
|
+
vtableLength,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function assertRootFlatbufferTable(buffer, identifier, label) {
|
|
169
|
+
assertFlatbufferIdentifier(buffer, identifier, label);
|
|
170
|
+
const rootTableStart = readUint32LE(buffer, 0, `${label} root offset`);
|
|
171
|
+
if (
|
|
172
|
+
!Number.isSafeInteger(rootTableStart) ||
|
|
173
|
+
rootTableStart < flatbuffers.SIZEOF_INT + flatbuffers.FILE_IDENTIFIER_LENGTH ||
|
|
174
|
+
rootTableStart > buffer.length - 4
|
|
175
|
+
) {
|
|
176
|
+
throw new Error(`${label} root offset is invalid.`);
|
|
177
|
+
}
|
|
178
|
+
return assertFlatbufferTable(buffer, rootTableStart, label);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertOptionalStringField(buffer, tableMeta, vtableFieldOffset, label) {
|
|
182
|
+
const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
|
|
183
|
+
if (fieldOffset === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const fieldStart = tableMeta.tableStart + fieldOffset;
|
|
187
|
+
const stringStart = resolveRelativeOffset(buffer, fieldStart, label);
|
|
188
|
+
const stringLength = readUint32LE(buffer, stringStart, `${label} length`);
|
|
189
|
+
assertBounds(buffer, stringStart + 4, stringLength, `${label} data`);
|
|
190
|
+
return {
|
|
191
|
+
fieldStart,
|
|
192
|
+
stringStart,
|
|
193
|
+
stringLength,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function assertOptionalByteVectorField(
|
|
198
|
+
buffer,
|
|
199
|
+
tableMeta,
|
|
200
|
+
vtableFieldOffset,
|
|
201
|
+
label,
|
|
202
|
+
{ minLength = 0, maxLength = Number.MAX_SAFE_INTEGER } = {},
|
|
203
|
+
) {
|
|
204
|
+
const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
|
|
205
|
+
if (fieldOffset === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const fieldStart = tableMeta.tableStart + fieldOffset;
|
|
209
|
+
const vectorStart = resolveRelativeOffset(buffer, fieldStart, label);
|
|
210
|
+
const vectorLength = readUint32LE(buffer, vectorStart, `${label} length`);
|
|
211
|
+
if (vectorLength < minLength || vectorLength > maxLength) {
|
|
212
|
+
throw new Error(`${label} length is invalid.`);
|
|
213
|
+
}
|
|
214
|
+
assertBounds(buffer, vectorStart + 4, vectorLength, `${label} data`);
|
|
215
|
+
return {
|
|
216
|
+
fieldStart,
|
|
217
|
+
vectorStart,
|
|
218
|
+
vectorLength,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertTableVectorField(buffer, tableMeta, vtableFieldOffset, label) {
|
|
223
|
+
const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
|
|
224
|
+
if (fieldOffset === 0) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const fieldStart = tableMeta.tableStart + fieldOffset;
|
|
228
|
+
const vectorStart = resolveRelativeOffset(buffer, fieldStart, label);
|
|
229
|
+
const vectorLength = readUint32LE(buffer, vectorStart, `${label} length`);
|
|
230
|
+
const vectorDataStart = vectorStart + 4;
|
|
231
|
+
assertBounds(
|
|
232
|
+
buffer,
|
|
233
|
+
vectorDataStart,
|
|
234
|
+
vectorLength * 4,
|
|
235
|
+
`${label} offsets`,
|
|
236
|
+
);
|
|
237
|
+
const elements = [];
|
|
238
|
+
for (let index = 0; index < vectorLength; index += 1) {
|
|
239
|
+
const elementOffset = vectorDataStart + index * 4;
|
|
240
|
+
const tableStart = resolveRelativeOffset(
|
|
241
|
+
buffer,
|
|
242
|
+
elementOffset,
|
|
243
|
+
`${label}[${index}]`,
|
|
244
|
+
);
|
|
245
|
+
elements.push(
|
|
246
|
+
assertFlatbufferTable(buffer, tableStart, `${label}[${index}]`),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return elements;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertUnionTableField(buffer, tableMeta, vtableFieldOffset, label) {
|
|
253
|
+
const fieldOffset = readTableFieldOffset(buffer, tableMeta, vtableFieldOffset);
|
|
254
|
+
if (fieldOffset === 0) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const fieldStart = tableMeta.tableStart + fieldOffset;
|
|
258
|
+
const tableStart = resolveRelativeOffset(buffer, fieldStart, label);
|
|
259
|
+
return assertFlatbufferTable(buffer, tableStart, label);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateEncTable(table, buffer, label) {
|
|
263
|
+
const tableMeta = assertFlatbufferTable(buffer, table.bb_pos, label);
|
|
264
|
+
assertOptionalByteVectorField(buffer, tableMeta, 12, `${label} ephemeral public key`, {
|
|
265
|
+
minLength: 1,
|
|
266
|
+
maxLength: 65,
|
|
267
|
+
});
|
|
268
|
+
assertOptionalByteVectorField(buffer, tableMeta, 14, `${label} nonce start`, {
|
|
269
|
+
minLength: 12,
|
|
270
|
+
maxLength: 12,
|
|
271
|
+
});
|
|
272
|
+
assertOptionalByteVectorField(buffer, tableMeta, 16, `${label} recipient key id`, {
|
|
273
|
+
maxLength: 32,
|
|
274
|
+
});
|
|
275
|
+
assertOptionalStringField(buffer, tableMeta, 18, `${label} context`);
|
|
276
|
+
assertOptionalByteVectorField(buffer, tableMeta, 20, `${label} schema hash`, {
|
|
277
|
+
maxLength: 32,
|
|
278
|
+
});
|
|
279
|
+
assertOptionalStringField(buffer, tableMeta, 22, `${label} root type`);
|
|
280
|
+
const record = normalizeEncTable(table.unpack());
|
|
281
|
+
if (!record.ephemeralPublicKey?.length) {
|
|
282
|
+
throw new Error(`${label} is missing the ephemeral public key.`);
|
|
283
|
+
}
|
|
284
|
+
if (!record.nonceStart || record.nonceStart.length !== 12) {
|
|
285
|
+
throw new Error(`${label} nonce start must be 12 bytes.`);
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
record.keyExchange === "X25519" &&
|
|
289
|
+
record.ephemeralPublicKey.length !== 32
|
|
290
|
+
) {
|
|
291
|
+
throw new Error(`${label} X25519 ephemeral public key must be 32 bytes.`);
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
record.keyExchange !== "X25519" &&
|
|
295
|
+
(record.ephemeralPublicKey.length < 32 || record.ephemeralPublicKey.length > 65)
|
|
296
|
+
) {
|
|
297
|
+
throw new Error(`${label} ephemeral public key length is invalid.`);
|
|
298
|
+
}
|
|
299
|
+
if (record.recipientKeyId && record.recipientKeyId.length > 32) {
|
|
300
|
+
throw new Error(`${label} recipient key id is too large.`);
|
|
301
|
+
}
|
|
302
|
+
if (record.schemaHash && record.schemaHash.length !== 32) {
|
|
303
|
+
throw new Error(`${label} schema hash must be 32 bytes when present.`);
|
|
304
|
+
}
|
|
305
|
+
return record;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function validatePnmTable(table, buffer, label) {
|
|
309
|
+
const tableMeta = assertFlatbufferTable(buffer, table.bb_pos, label);
|
|
310
|
+
assertOptionalStringField(buffer, tableMeta, 4, `${label} multiformat address`);
|
|
311
|
+
assertOptionalStringField(buffer, tableMeta, 6, `${label} publish timestamp`);
|
|
312
|
+
assertOptionalStringField(buffer, tableMeta, 8, `${label} cid`);
|
|
313
|
+
assertOptionalStringField(buffer, tableMeta, 10, `${label} file name`);
|
|
314
|
+
assertOptionalStringField(buffer, tableMeta, 12, `${label} file id`);
|
|
315
|
+
assertOptionalStringField(buffer, tableMeta, 14, `${label} signature`);
|
|
316
|
+
assertOptionalStringField(buffer, tableMeta, 16, `${label} timestamp signature`);
|
|
317
|
+
assertOptionalStringField(buffer, tableMeta, 18, `${label} signature type`);
|
|
318
|
+
assertOptionalStringField(buffer, tableMeta, 20, `${label} timestamp signature type`);
|
|
319
|
+
const record = normalizePnmTable(table.unpack());
|
|
320
|
+
if (
|
|
321
|
+
!record.multiformatAddress &&
|
|
322
|
+
!record.publishTimestamp &&
|
|
323
|
+
!record.cid &&
|
|
324
|
+
!record.fileName &&
|
|
325
|
+
!record.fileId &&
|
|
326
|
+
!record.signature &&
|
|
327
|
+
!record.timestampSignature &&
|
|
328
|
+
!record.signatureType &&
|
|
329
|
+
!record.timestampSignatureType
|
|
330
|
+
) {
|
|
331
|
+
throw new Error(`${label} must contain at least one populated field.`);
|
|
332
|
+
}
|
|
333
|
+
return record;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function concatBytes(chunks) {
|
|
337
|
+
const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
338
|
+
const out = new Uint8Array(totalLength);
|
|
339
|
+
let offset = 0;
|
|
340
|
+
for (const chunk of chunks) {
|
|
341
|
+
out.set(chunk, offset);
|
|
342
|
+
offset += chunk.length;
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function normalizeByteField(value) {
|
|
348
|
+
if (value === undefined || value === null) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const normalized = toUint8Array(value);
|
|
352
|
+
return normalized.length > 0 ? normalized : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeStringField(value) {
|
|
356
|
+
if (value === undefined || value === null) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const normalized = String(value).trim();
|
|
360
|
+
return normalized.length > 0 ? normalized : null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeKeyExchange(value) {
|
|
364
|
+
if (typeof value === "number") {
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
return KEY_EXCHANGE_BY_NAME[
|
|
368
|
+
String(value ?? "X25519")
|
|
369
|
+
.trim()
|
|
370
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
371
|
+
.toUpperCase()
|
|
372
|
+
] ?? KeyExchange.X25519;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizeSymmetricAlgorithm(value) {
|
|
376
|
+
if (typeof value === "number") {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
return SYMMETRIC_ALGO_BY_NAME[
|
|
380
|
+
String(value ?? "AES_256_CTR")
|
|
381
|
+
.trim()
|
|
382
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
383
|
+
.toUpperCase()
|
|
384
|
+
] ?? SymmetricAlgo.AES_256_CTR;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function normalizeKdf(value) {
|
|
388
|
+
if (typeof value === "number") {
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
return KDF_BY_NAME[
|
|
392
|
+
String(value ?? "HKDF_SHA256")
|
|
393
|
+
.trim()
|
|
394
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
395
|
+
.toUpperCase()
|
|
396
|
+
] ?? KDF.HKDF_SHA256;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function encTableFromObject(record = {}) {
|
|
400
|
+
return new ENCT(
|
|
401
|
+
Number(record.version ?? 1),
|
|
402
|
+
normalizeKeyExchange(record.keyExchange),
|
|
403
|
+
normalizeSymmetricAlgorithm(record.symmetric),
|
|
404
|
+
normalizeKdf(record.keyDerivation),
|
|
405
|
+
Array.from(toUint8Array(record.ephemeralPublicKey)),
|
|
406
|
+
Array.from(toUint8Array(record.nonceStart)),
|
|
407
|
+
Array.from(normalizeByteField(record.recipientKeyId) ?? []),
|
|
408
|
+
normalizeStringField(record.context),
|
|
409
|
+
Array.from(normalizeByteField(record.schemaHash) ?? []),
|
|
410
|
+
normalizeStringField(record.rootType),
|
|
411
|
+
BigInt(record.timestamp ?? 0),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function pnmTableFromObject(record = {}) {
|
|
416
|
+
return new PNMT(
|
|
417
|
+
normalizeStringField(record.multiformatAddress),
|
|
418
|
+
normalizeStringField(record.publishTimestamp),
|
|
419
|
+
normalizeStringField(record.cid),
|
|
420
|
+
normalizeStringField(record.fileName),
|
|
421
|
+
normalizeStringField(record.fileId),
|
|
422
|
+
normalizeStringField(record.signature),
|
|
423
|
+
normalizeStringField(record.timestampSignature),
|
|
424
|
+
normalizeStringField(record.signatureType),
|
|
425
|
+
normalizeStringField(record.timestampSignatureType),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function normalizeEncTable(table) {
|
|
430
|
+
if (!table) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
version: Number(table.VERSION ?? 1),
|
|
435
|
+
keyExchange:
|
|
436
|
+
KEY_EXCHANGE_NAME_BY_VALUE[table.KEY_EXCHANGE] ?? String(table.KEY_EXCHANGE),
|
|
437
|
+
symmetric:
|
|
438
|
+
SYMMETRIC_ALGO_NAME_BY_VALUE[table.SYMMETRIC] ?? String(table.SYMMETRIC),
|
|
439
|
+
keyDerivation:
|
|
440
|
+
KDF_NAME_BY_VALUE[table.KEY_DERIVATION] ?? String(table.KEY_DERIVATION),
|
|
441
|
+
ephemeralPublicKey: normalizeByteField(table.EPHEMERAL_PUBLIC_KEY),
|
|
442
|
+
nonceStart: normalizeByteField(table.NONCE_START),
|
|
443
|
+
recipientKeyId: normalizeByteField(table.RECIPIENT_KEY_ID),
|
|
444
|
+
context: normalizeStringField(table.CONTEXT),
|
|
445
|
+
schemaHash: normalizeByteField(table.SCHEMA_HASH),
|
|
446
|
+
rootType: normalizeStringField(table.ROOT_TYPE),
|
|
447
|
+
timestamp:
|
|
448
|
+
table.TIMESTAMP === undefined || table.TIMESTAMP === null
|
|
449
|
+
? 0
|
|
450
|
+
: Number(table.TIMESTAMP),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizePnmTable(table) {
|
|
455
|
+
if (!table) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
multiformatAddress: normalizeStringField(table.MULTIFORMAT_ADDRESS),
|
|
460
|
+
publishTimestamp: normalizeStringField(table.PUBLISH_TIMESTAMP),
|
|
461
|
+
cid: normalizeStringField(table.CID),
|
|
462
|
+
fileName: normalizeStringField(table.FILE_NAME),
|
|
463
|
+
fileId: normalizeStringField(table.FILE_ID),
|
|
464
|
+
signature: normalizeStringField(table.SIGNATURE),
|
|
465
|
+
timestampSignature: normalizeStringField(table.TIMESTAMP_SIGNATURE),
|
|
466
|
+
signatureType: normalizeStringField(table.SIGNATURE_TYPE),
|
|
467
|
+
timestampSignatureType: normalizeStringField(table.TIMESTAMP_SIGNATURE_TYPE),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function readFooterLength(bytes) {
|
|
472
|
+
const view = toUint8Array(bytes);
|
|
473
|
+
if (view.length < TRAILER_FOOTER_LENGTH) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const footerOffset = view.length - TRAILER_FOOTER_LENGTH;
|
|
477
|
+
for (let index = 0; index < TRAILER_MAGIC_BYTES.length; index += 1) {
|
|
478
|
+
if (view[footerOffset + 4 + index] !== TRAILER_MAGIC_BYTES[index]) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return new DataView(
|
|
483
|
+
view.buffer,
|
|
484
|
+
view.byteOffset + footerOffset,
|
|
485
|
+
TRAILER_FOOTER_LENGTH,
|
|
486
|
+
).getUint32(0, true);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function encodeFooter(recordCollectionLength) {
|
|
490
|
+
if (
|
|
491
|
+
!Number.isSafeInteger(recordCollectionLength) ||
|
|
492
|
+
recordCollectionLength < 0 ||
|
|
493
|
+
recordCollectionLength > 0xffff_ffff
|
|
494
|
+
) {
|
|
495
|
+
throw new RangeError("REC trailer length must fit in uint32.");
|
|
496
|
+
}
|
|
497
|
+
const footer = new Uint8Array(TRAILER_FOOTER_LENGTH);
|
|
498
|
+
const view = new DataView(footer.buffer);
|
|
499
|
+
view.setUint32(0, recordCollectionLength, true);
|
|
500
|
+
footer.set(TRAILER_MAGIC_BYTES, 4);
|
|
501
|
+
return footer;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function toBase32Lower(bytes) {
|
|
505
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
506
|
+
let bits = 0;
|
|
507
|
+
let value = 0;
|
|
508
|
+
let out = "";
|
|
509
|
+
for (const byte of toUint8Array(bytes)) {
|
|
510
|
+
value = (value << 8) | byte;
|
|
511
|
+
bits += 8;
|
|
512
|
+
while (bits >= 5) {
|
|
513
|
+
out += alphabet[(value >>> (bits - 5)) & 31];
|
|
514
|
+
bits -= 5;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (bits > 0) {
|
|
518
|
+
out += alphabet[(value << (5 - bits)) & 31];
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function createCidV1Raw(payloadBytes) {
|
|
524
|
+
const digest = await sha256Bytes(payloadBytes);
|
|
525
|
+
const cidBytes = concatBytes([
|
|
526
|
+
Uint8Array.of(0x01), // cidv1
|
|
527
|
+
Uint8Array.of(0x55), // raw
|
|
528
|
+
Uint8Array.of(0x12, digest.length), // sha2-256 multihash
|
|
529
|
+
digest,
|
|
530
|
+
]);
|
|
531
|
+
return `b${toBase32Lower(cidBytes)}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function encodeEncRecord(record = {}) {
|
|
535
|
+
const builder = new flatbuffers.Builder(256);
|
|
536
|
+
const table = encTableFromObject(record);
|
|
537
|
+
const root = table.pack(builder);
|
|
538
|
+
ENC.finishENCBuffer(builder, root);
|
|
539
|
+
return builder.asUint8Array();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function decodeEncRecord(bytes) {
|
|
543
|
+
const buffer = toUint8Array(bytes);
|
|
544
|
+
assertRootFlatbufferTable(buffer, "$ENC", "ENC record");
|
|
545
|
+
const bb = new flatbuffers.ByteBuffer(buffer);
|
|
546
|
+
return validateEncTable(ENC.getRootAsENC(bb), buffer, "ENC record");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function encodePnmRecord(record = {}) {
|
|
550
|
+
const builder = new flatbuffers.Builder(256);
|
|
551
|
+
const table = pnmTableFromObject(record);
|
|
552
|
+
const root = table.pack(builder);
|
|
553
|
+
PNM.finishPNMBuffer(builder, root);
|
|
554
|
+
return builder.asUint8Array();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function decodePnmRecord(bytes) {
|
|
558
|
+
const buffer = toUint8Array(bytes);
|
|
559
|
+
assertRootFlatbufferTable(buffer, "$PNM", "PNM record");
|
|
560
|
+
const bb = new flatbuffers.ByteBuffer(buffer);
|
|
561
|
+
return validatePnmTable(PNM.getRootAsPNM(bb), buffer, "PNM record");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function encodePublicationRecordCollection(options = {}) {
|
|
565
|
+
const records = [];
|
|
566
|
+
if (options.enc) {
|
|
567
|
+
records.push(new RecordT(RECORD_TYPE_BY_STANDARD.ENC, encTableFromObject(options.enc), "ENC"));
|
|
568
|
+
}
|
|
569
|
+
if (options.pnm) {
|
|
570
|
+
records.push(new RecordT(RECORD_TYPE_BY_STANDARD.PNM, pnmTableFromObject(options.pnm), "PNM"));
|
|
571
|
+
}
|
|
572
|
+
if (records.length === 0) {
|
|
573
|
+
throw new Error("At least one ENC or PNM record is required.");
|
|
574
|
+
}
|
|
575
|
+
const builder = new flatbuffers.Builder(512);
|
|
576
|
+
const root = new RECT(
|
|
577
|
+
normalizeStringField(options.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
|
|
578
|
+
records,
|
|
579
|
+
).pack(builder);
|
|
580
|
+
REC.finishRECBuffer(builder, root);
|
|
581
|
+
return builder.asUint8Array();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function decodePublicationRecordCollection(bytes) {
|
|
585
|
+
const buffer = toUint8Array(bytes);
|
|
586
|
+
assertRootFlatbufferTable(buffer, "$REC", "REC trailer");
|
|
587
|
+
const bb = new flatbuffers.ByteBuffer(buffer);
|
|
588
|
+
const collectionTable = REC.getRootAsREC(bb);
|
|
589
|
+
const collectionMeta = assertFlatbufferTable(
|
|
590
|
+
buffer,
|
|
591
|
+
collectionTable.bb_pos,
|
|
592
|
+
"REC trailer",
|
|
593
|
+
);
|
|
594
|
+
assertOptionalStringField(buffer, collectionMeta, 4, "REC trailer version");
|
|
595
|
+
const recordTables = assertTableVectorField(
|
|
596
|
+
buffer,
|
|
597
|
+
collectionMeta,
|
|
598
|
+
6,
|
|
599
|
+
"REC trailer records",
|
|
600
|
+
);
|
|
601
|
+
if (recordTables.length === 0) {
|
|
602
|
+
throw new Error("REC trailer does not contain any records.");
|
|
603
|
+
}
|
|
604
|
+
const collection = collectionTable.unpack();
|
|
605
|
+
const records = [];
|
|
606
|
+
let enc = null;
|
|
607
|
+
let pnm = null;
|
|
608
|
+
for (let index = 0; index < recordTables.length; index += 1) {
|
|
609
|
+
const recordTable =
|
|
610
|
+
collectionTable.RECORDS(index, new Record()) ?? null;
|
|
611
|
+
if (!recordTable) {
|
|
612
|
+
throw new Error(`REC trailer record ${index} could not be loaded.`);
|
|
613
|
+
}
|
|
614
|
+
const recordMeta = assertFlatbufferTable(
|
|
615
|
+
buffer,
|
|
616
|
+
recordTable.bb_pos,
|
|
617
|
+
`REC trailer record ${index}`,
|
|
618
|
+
);
|
|
619
|
+
assertOptionalStringField(
|
|
620
|
+
buffer,
|
|
621
|
+
recordMeta,
|
|
622
|
+
8,
|
|
623
|
+
`REC trailer record ${index} standard`,
|
|
624
|
+
);
|
|
625
|
+
const unpackedRecord = collection.RECORDS[index];
|
|
626
|
+
const recordType = recordTable.value_type();
|
|
627
|
+
const standard =
|
|
628
|
+
normalizeStringField(unpackedRecord?.standard) ??
|
|
629
|
+
STANDARD_BY_RECORD_TYPE[recordType] ??
|
|
630
|
+
null;
|
|
631
|
+
const expectedStandard =
|
|
632
|
+
STANDARD_BY_RECORD_TYPE[recordType] ?? null;
|
|
633
|
+
if (standard && expectedStandard && standard !== expectedStandard) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`REC trailer record ${index} standard/type mismatch (${standard} vs ${expectedStandard}).`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const valueMeta = assertUnionTableField(
|
|
639
|
+
buffer,
|
|
640
|
+
recordMeta,
|
|
641
|
+
6,
|
|
642
|
+
`REC trailer record ${index} value`,
|
|
643
|
+
);
|
|
644
|
+
if (!valueMeta) {
|
|
645
|
+
throw new Error(`REC trailer record ${index} is missing a value.`);
|
|
646
|
+
}
|
|
647
|
+
let value =
|
|
648
|
+
standard === "ENC" || standard === "PNM"
|
|
649
|
+
? null
|
|
650
|
+
: unpackedRecord?.value ?? null;
|
|
651
|
+
if (standard === "ENC") {
|
|
652
|
+
const encTable = recordTable.value(new ENC());
|
|
653
|
+
if (!encTable) {
|
|
654
|
+
throw new Error(`REC trailer record ${index} ENC payload is missing.`);
|
|
655
|
+
}
|
|
656
|
+
if (enc) {
|
|
657
|
+
throw new Error("REC trailer contains multiple ENC records.");
|
|
658
|
+
}
|
|
659
|
+
enc = validateEncTable(
|
|
660
|
+
encTable,
|
|
661
|
+
buffer,
|
|
662
|
+
`REC trailer record ${index} ENC payload`,
|
|
663
|
+
);
|
|
664
|
+
value = enc;
|
|
665
|
+
} else if (standard === "PNM") {
|
|
666
|
+
const pnmTable = recordTable.value(new PNM());
|
|
667
|
+
if (!pnmTable) {
|
|
668
|
+
throw new Error(`REC trailer record ${index} PNM payload is missing.`);
|
|
669
|
+
}
|
|
670
|
+
if (pnm) {
|
|
671
|
+
throw new Error("REC trailer contains multiple PNM records.");
|
|
672
|
+
}
|
|
673
|
+
pnm = validatePnmTable(
|
|
674
|
+
pnmTable,
|
|
675
|
+
buffer,
|
|
676
|
+
`REC trailer record ${index} PNM payload`,
|
|
677
|
+
);
|
|
678
|
+
value = pnm;
|
|
679
|
+
}
|
|
680
|
+
records.push({
|
|
681
|
+
standard,
|
|
682
|
+
recordType,
|
|
683
|
+
value,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
version: normalizeStringField(collection.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
|
|
688
|
+
records,
|
|
689
|
+
enc,
|
|
690
|
+
pnm,
|
|
691
|
+
recordCollectionBytes: buffer,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function appendPublicationRecordCollection(
|
|
696
|
+
payloadBytes,
|
|
697
|
+
recordCollectionBytes,
|
|
698
|
+
) {
|
|
699
|
+
const payload = toUint8Array(payloadBytes);
|
|
700
|
+
const recordCollection = toUint8Array(recordCollectionBytes);
|
|
701
|
+
return concatBytes([
|
|
702
|
+
payload,
|
|
703
|
+
recordCollection,
|
|
704
|
+
encodeFooter(recordCollection.length),
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function stripPublicationRecordCollection(bytes) {
|
|
709
|
+
const parsed = extractPublicationRecordCollection(bytes);
|
|
710
|
+
return parsed?.payloadBytes ?? toUint8Array(bytes);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function extractPublicationRecordCollection(bytes) {
|
|
714
|
+
const buffer = toUint8Array(bytes);
|
|
715
|
+
const recordCollectionLength = readFooterLength(buffer);
|
|
716
|
+
if (recordCollectionLength === null) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
const footerOffset = buffer.length - TRAILER_FOOTER_LENGTH;
|
|
720
|
+
const recordCollectionOffset = footerOffset - recordCollectionLength;
|
|
721
|
+
if (recordCollectionOffset < 0) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
const recordCollectionBytes = buffer.subarray(
|
|
725
|
+
recordCollectionOffset,
|
|
726
|
+
footerOffset,
|
|
727
|
+
);
|
|
728
|
+
try {
|
|
729
|
+
const decoded = decodePublicationRecordCollection(recordCollectionBytes);
|
|
730
|
+
return {
|
|
731
|
+
...decoded,
|
|
732
|
+
payloadBytes: buffer.subarray(0, recordCollectionOffset),
|
|
733
|
+
protectedBytes: buffer,
|
|
734
|
+
footerBytes: buffer.subarray(footerOffset),
|
|
735
|
+
footerMagic: TRAILER_MAGIC_TEXT,
|
|
736
|
+
recordCollectionLength,
|
|
737
|
+
};
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export async function createPublicationNotice(options = {}) {
|
|
744
|
+
const payloadBytes = toUint8Array(options.payloadBytes);
|
|
745
|
+
const cid = normalizeStringField(options.cid) ?? (await createCidV1Raw(payloadBytes));
|
|
746
|
+
const publishTimestamp =
|
|
747
|
+
normalizeStringField(options.publishTimestamp) ??
|
|
748
|
+
new Date(
|
|
749
|
+
Number.isFinite(options.publishTimestampMs)
|
|
750
|
+
? options.publishTimestampMs
|
|
751
|
+
: Date.now(),
|
|
752
|
+
).toISOString();
|
|
753
|
+
const fileName =
|
|
754
|
+
normalizeStringField(options.fileName) ??
|
|
755
|
+
normalizeStringField(options.artifactId) ??
|
|
756
|
+
"module.wasm";
|
|
757
|
+
const fileId =
|
|
758
|
+
normalizeStringField(options.fileId) ??
|
|
759
|
+
normalizeStringField(options.programId) ??
|
|
760
|
+
normalizeStringField(options.artifactId) ??
|
|
761
|
+
"module";
|
|
762
|
+
const multiformatAddress =
|
|
763
|
+
normalizeStringField(options.multiformatAddress) ?? `/ipfs/${cid}`;
|
|
764
|
+
|
|
765
|
+
let signature = normalizeStringField(options.signature);
|
|
766
|
+
let timestampSignature = normalizeStringField(options.timestampSignature);
|
|
767
|
+
let signatureType = normalizeStringField(options.signatureType);
|
|
768
|
+
let timestampSignatureType = normalizeStringField(options.timestampSignatureType);
|
|
769
|
+
if (options.signer && typeof options.signer.sign === "function") {
|
|
770
|
+
signature = bytesToHex(await options.signer.sign(textEncoder.encode(cid)));
|
|
771
|
+
timestampSignature = bytesToHex(
|
|
772
|
+
await options.signer.sign(textEncoder.encode(publishTimestamp)),
|
|
773
|
+
);
|
|
774
|
+
signatureType =
|
|
775
|
+
signatureType ??
|
|
776
|
+
normalizeStringField(options.signer.algorithm) ??
|
|
777
|
+
"unknown";
|
|
778
|
+
timestampSignatureType =
|
|
779
|
+
timestampSignatureType ??
|
|
780
|
+
normalizeStringField(options.signer.algorithm) ??
|
|
781
|
+
"unknown";
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
multiformatAddress,
|
|
786
|
+
publishTimestamp,
|
|
787
|
+
cid,
|
|
788
|
+
fileName,
|
|
789
|
+
fileId,
|
|
790
|
+
signature,
|
|
791
|
+
timestampSignature,
|
|
792
|
+
signatureType,
|
|
793
|
+
timestampSignatureType,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function createEncryptedEnvelopePayload(options = {}) {
|
|
798
|
+
const protectedBlob = toUint8Array(options.protectedBlobBytes);
|
|
799
|
+
const parsed =
|
|
800
|
+
options.parsedProtectedBlob ?? extractPublicationRecordCollection(protectedBlob);
|
|
801
|
+
const enc = options.enc ?? parsed?.enc ?? null;
|
|
802
|
+
const envelope = {
|
|
803
|
+
version: Number(options.version ?? 2),
|
|
804
|
+
scheme:
|
|
805
|
+
normalizeStringField(options.scheme) ?? "x25519-hkdf-aes-256-ctr-rec",
|
|
806
|
+
context: normalizeStringField(options.context ?? enc?.context) ?? "",
|
|
807
|
+
protectedBlobBase64: bytesToBase64(protectedBlob),
|
|
808
|
+
recordCollectionBase64: parsed
|
|
809
|
+
? bytesToBase64(parsed.recordCollectionBytes)
|
|
810
|
+
: null,
|
|
811
|
+
ciphertextBase64: parsed ? bytesToBase64(parsed.payloadBytes) : null,
|
|
812
|
+
};
|
|
813
|
+
if (enc?.ephemeralPublicKey) {
|
|
814
|
+
envelope.senderPublicKeyBase64 = bytesToBase64(enc.ephemeralPublicKey);
|
|
815
|
+
}
|
|
816
|
+
if (enc?.nonceStart) {
|
|
817
|
+
envelope.nonceStartBase64 = bytesToBase64(enc.nonceStart);
|
|
818
|
+
}
|
|
819
|
+
if (enc?.recipientKeyId) {
|
|
820
|
+
envelope.recipientKeyIdBase64 = bytesToBase64(enc.recipientKeyId);
|
|
821
|
+
}
|
|
822
|
+
if (enc) {
|
|
823
|
+
envelope.encRecordBase64 = bytesToBase64(encodeEncRecord(enc));
|
|
824
|
+
}
|
|
825
|
+
if (parsed?.pnm) {
|
|
826
|
+
envelope.pnmRecordBase64 = bytesToBase64(encodePnmRecord(parsed.pnm));
|
|
827
|
+
}
|
|
828
|
+
return envelope;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function decodeProtectedBlobBase64(base64) {
|
|
832
|
+
const bytes = base64ToBytes(base64);
|
|
833
|
+
return extractPublicationRecordCollection(bytes);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export { TRAILER_MAGIC_TEXT, TRAILER_FOOTER_LENGTH };
|