space-data-module-sdk 0.4.1 → 0.5.3

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.
@@ -6,6 +6,8 @@ import {
6
6
  toUint8Array,
7
7
  } from "../utils/encoding.js";
8
8
  import {
9
+ aesCtrDecrypt,
10
+ aesCtrEncrypt,
9
11
  aesGcmDecrypt,
10
12
  aesGcmEncrypt,
11
13
  hkdfBytes,
@@ -13,6 +15,13 @@ import {
13
15
  x25519PublicKey,
14
16
  x25519SharedSecret,
15
17
  } from "../utils/wasmCrypto.js";
18
+ import {
19
+ appendPublicationRecordCollection,
20
+ createEncryptedEnvelopePayload,
21
+ decodeEncRecord,
22
+ encodePublicationRecordCollection,
23
+ extractPublicationRecordCollection,
24
+ } from "./records.js";
16
25
 
17
26
  function normalizePublicKey(value) {
18
27
  if (typeof value === "string") {
@@ -53,15 +62,12 @@ export async function generateX25519Keypair() {
53
62
  };
54
63
  }
55
64
 
56
- export async function encryptBytesForRecipient({
65
+ async function encryptBytesLegacy({
57
66
  plaintext,
58
67
  recipientPublicKey,
59
68
  context = "space-data-module-sdk/package",
60
69
  senderKeyPair = null,
61
70
  } = {}) {
62
- if (!recipientPublicKey) {
63
- throw new Error("encryptBytesForRecipient requires recipientPublicKey.");
64
- }
65
71
  const sender = senderKeyPair ?? (await generateX25519Keypair());
66
72
  const salt = await randomBytes(32);
67
73
  const iv = await randomBytes(12);
@@ -89,6 +95,77 @@ export async function encryptBytesForRecipient({
89
95
  };
90
96
  }
91
97
 
98
+ export async function decryptProtectedBytes({
99
+ protectedBytes,
100
+ recipientPrivateKey,
101
+ } = {}) {
102
+ const parsed = extractPublicationRecordCollection(protectedBytes);
103
+ if (!parsed?.enc) {
104
+ return toUint8Array(protectedBytes);
105
+ }
106
+ const sharedSecret = await deriveSharedSecret(
107
+ recipientPrivateKey,
108
+ parsed.enc.ephemeralPublicKey,
109
+ );
110
+ const aesKey = await deriveAesKey(
111
+ sharedSecret,
112
+ new Uint8Array(0),
113
+ parsed.enc.context ?? "",
114
+ );
115
+ return aesCtrDecrypt(aesKey, parsed.payloadBytes, parsed.enc.nonceStart);
116
+ }
117
+
118
+ export async function encryptBytesForRecipient({
119
+ plaintext,
120
+ recipientPublicKey,
121
+ context = "space-data-module-sdk/package",
122
+ senderKeyPair = null,
123
+ recipientKeyId = null,
124
+ schemaHash = null,
125
+ rootType = null,
126
+ } = {}) {
127
+ if (!recipientPublicKey) {
128
+ throw new Error("encryptBytesForRecipient requires recipientPublicKey.");
129
+ }
130
+ const sender = senderKeyPair ?? (await generateX25519Keypair());
131
+ const nonceStart = await randomBytes(12);
132
+ const sharedSecret = await deriveSharedSecret(
133
+ sender.privateKey,
134
+ recipientPublicKey,
135
+ );
136
+ const aesKey = await deriveAesKey(sharedSecret, new Uint8Array(0), context);
137
+ const ciphertext = await aesCtrEncrypt(aesKey, toUint8Array(plaintext), nonceStart);
138
+ const enc = {
139
+ version: 1,
140
+ keyExchange: "X25519",
141
+ symmetric: "AES_256_CTR",
142
+ keyDerivation: "HKDF_SHA256",
143
+ ephemeralPublicKey: sender.publicKey,
144
+ nonceStart,
145
+ recipientKeyId,
146
+ context,
147
+ schemaHash,
148
+ rootType,
149
+ timestamp: Date.now(),
150
+ };
151
+ const recordCollectionBytes = encodePublicationRecordCollection({ enc });
152
+ const protectedBlobBytes = appendPublicationRecordCollection(
153
+ ciphertext,
154
+ recordCollectionBytes,
155
+ );
156
+ return createEncryptedEnvelopePayload({
157
+ protectedBlobBytes,
158
+ parsedProtectedBlob: {
159
+ payloadBytes: ciphertext,
160
+ recordCollectionBytes,
161
+ enc,
162
+ pnm: null,
163
+ },
164
+ enc,
165
+ context,
166
+ });
167
+ }
168
+
92
169
  export async function decryptBytesFromEnvelope({
93
170
  envelope,
94
171
  recipientPrivateKey,
@@ -98,6 +175,29 @@ export async function decryptBytesFromEnvelope({
98
175
  "decryptBytesFromEnvelope requires envelope and recipientPrivateKey.",
99
176
  );
100
177
  }
178
+ if (envelope.protectedBlobBase64) {
179
+ return decryptProtectedBytes({
180
+ protectedBytes: base64ToBytes(envelope.protectedBlobBase64),
181
+ recipientPrivateKey,
182
+ });
183
+ }
184
+ if (envelope.ciphertextBase64 && envelope.encRecordBase64) {
185
+ const enc = decodeEncRecord(base64ToBytes(envelope.encRecordBase64));
186
+ const sharedSecret = await deriveSharedSecret(
187
+ recipientPrivateKey,
188
+ enc.ephemeralPublicKey,
189
+ );
190
+ const aesKey = await deriveAesKey(
191
+ sharedSecret,
192
+ new Uint8Array(0),
193
+ enc.context ?? envelope.context ?? "",
194
+ );
195
+ return aesCtrDecrypt(
196
+ aesKey,
197
+ base64ToBytes(envelope.ciphertextBase64),
198
+ enc.nonceStart,
199
+ );
200
+ }
101
201
  const sharedSecret = await deriveSharedSecret(
102
202
  recipientPrivateKey,
103
203
  base64ToBytes(envelope.senderPublicKeyBase64),
@@ -133,3 +233,24 @@ export async function decryptJsonFromEnvelope(options = {}) {
133
233
  return JSON.parse(new TextDecoder().decode(bytes));
134
234
  }
135
235
 
236
+ export async function decryptPublicationRecordCollection({
237
+ protectedBytes,
238
+ recipientPrivateKey,
239
+ } = {}) {
240
+ const parsed = extractPublicationRecordCollection(protectedBytes);
241
+ if (!parsed) {
242
+ return {
243
+ payloadBytes: toUint8Array(protectedBytes),
244
+ decryptedBytes: toUint8Array(protectedBytes),
245
+ publication: null,
246
+ };
247
+ }
248
+ const decryptedBytes = parsed.enc
249
+ ? await decryptProtectedBytes({ protectedBytes, recipientPrivateKey })
250
+ : parsed.payloadBytes;
251
+ return {
252
+ payloadBytes: parsed.payloadBytes,
253
+ decryptedBytes,
254
+ publication: parsed,
255
+ };
256
+ }
@@ -0,0 +1,493 @@
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 { 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 concatBytes(chunks) {
62
+ const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);
63
+ const out = new Uint8Array(totalLength);
64
+ let offset = 0;
65
+ for (const chunk of chunks) {
66
+ out.set(chunk, offset);
67
+ offset += chunk.length;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function normalizeByteField(value) {
73
+ if (value === undefined || value === null) {
74
+ return null;
75
+ }
76
+ return toUint8Array(value);
77
+ }
78
+
79
+ function normalizeStringField(value) {
80
+ if (value === undefined || value === null) {
81
+ return null;
82
+ }
83
+ const normalized = String(value).trim();
84
+ return normalized.length > 0 ? normalized : null;
85
+ }
86
+
87
+ function normalizeKeyExchange(value) {
88
+ if (typeof value === "number") {
89
+ return value;
90
+ }
91
+ return KEY_EXCHANGE_BY_NAME[
92
+ String(value ?? "X25519")
93
+ .trim()
94
+ .replace(/[^A-Za-z0-9]+/g, "_")
95
+ .toUpperCase()
96
+ ] ?? KeyExchange.X25519;
97
+ }
98
+
99
+ function normalizeSymmetricAlgorithm(value) {
100
+ if (typeof value === "number") {
101
+ return value;
102
+ }
103
+ return SYMMETRIC_ALGO_BY_NAME[
104
+ String(value ?? "AES_256_CTR")
105
+ .trim()
106
+ .replace(/[^A-Za-z0-9]+/g, "_")
107
+ .toUpperCase()
108
+ ] ?? SymmetricAlgo.AES_256_CTR;
109
+ }
110
+
111
+ function normalizeKdf(value) {
112
+ if (typeof value === "number") {
113
+ return value;
114
+ }
115
+ return KDF_BY_NAME[
116
+ String(value ?? "HKDF_SHA256")
117
+ .trim()
118
+ .replace(/[^A-Za-z0-9]+/g, "_")
119
+ .toUpperCase()
120
+ ] ?? KDF.HKDF_SHA256;
121
+ }
122
+
123
+ function encTableFromObject(record = {}) {
124
+ return new ENCT(
125
+ Number(record.version ?? 1),
126
+ normalizeKeyExchange(record.keyExchange),
127
+ normalizeSymmetricAlgorithm(record.symmetric),
128
+ normalizeKdf(record.keyDerivation),
129
+ Array.from(toUint8Array(record.ephemeralPublicKey)),
130
+ Array.from(toUint8Array(record.nonceStart)),
131
+ Array.from(normalizeByteField(record.recipientKeyId) ?? []),
132
+ normalizeStringField(record.context),
133
+ Array.from(normalizeByteField(record.schemaHash) ?? []),
134
+ normalizeStringField(record.rootType),
135
+ BigInt(record.timestamp ?? 0),
136
+ );
137
+ }
138
+
139
+ function pnmTableFromObject(record = {}) {
140
+ return new PNMT(
141
+ normalizeStringField(record.multiformatAddress),
142
+ normalizeStringField(record.publishTimestamp),
143
+ normalizeStringField(record.cid),
144
+ normalizeStringField(record.fileName),
145
+ normalizeStringField(record.fileId),
146
+ normalizeStringField(record.signature),
147
+ normalizeStringField(record.timestampSignature),
148
+ normalizeStringField(record.signatureType),
149
+ normalizeStringField(record.timestampSignatureType),
150
+ );
151
+ }
152
+
153
+ function normalizeEncTable(table) {
154
+ if (!table) {
155
+ return null;
156
+ }
157
+ return {
158
+ version: Number(table.VERSION ?? 1),
159
+ keyExchange:
160
+ KEY_EXCHANGE_NAME_BY_VALUE[table.KEY_EXCHANGE] ?? String(table.KEY_EXCHANGE),
161
+ symmetric:
162
+ SYMMETRIC_ALGO_NAME_BY_VALUE[table.SYMMETRIC] ?? String(table.SYMMETRIC),
163
+ keyDerivation:
164
+ KDF_NAME_BY_VALUE[table.KEY_DERIVATION] ?? String(table.KEY_DERIVATION),
165
+ ephemeralPublicKey: normalizeByteField(table.EPHEMERAL_PUBLIC_KEY),
166
+ nonceStart: normalizeByteField(table.NONCE_START),
167
+ recipientKeyId: normalizeByteField(table.RECIPIENT_KEY_ID),
168
+ context: normalizeStringField(table.CONTEXT),
169
+ schemaHash: normalizeByteField(table.SCHEMA_HASH),
170
+ rootType: normalizeStringField(table.ROOT_TYPE),
171
+ timestamp:
172
+ table.TIMESTAMP === undefined || table.TIMESTAMP === null
173
+ ? 0
174
+ : Number(table.TIMESTAMP),
175
+ };
176
+ }
177
+
178
+ function normalizePnmTable(table) {
179
+ if (!table) {
180
+ return null;
181
+ }
182
+ return {
183
+ multiformatAddress: normalizeStringField(table.MULTIFORMAT_ADDRESS),
184
+ publishTimestamp: normalizeStringField(table.PUBLISH_TIMESTAMP),
185
+ cid: normalizeStringField(table.CID),
186
+ fileName: normalizeStringField(table.FILE_NAME),
187
+ fileId: normalizeStringField(table.FILE_ID),
188
+ signature: normalizeStringField(table.SIGNATURE),
189
+ timestampSignature: normalizeStringField(table.TIMESTAMP_SIGNATURE),
190
+ signatureType: normalizeStringField(table.SIGNATURE_TYPE),
191
+ timestampSignatureType: normalizeStringField(table.TIMESTAMP_SIGNATURE_TYPE),
192
+ };
193
+ }
194
+
195
+ function readFooterLength(bytes) {
196
+ const view = toUint8Array(bytes);
197
+ if (view.length < TRAILER_FOOTER_LENGTH) {
198
+ return null;
199
+ }
200
+ const footerOffset = view.length - TRAILER_FOOTER_LENGTH;
201
+ for (let index = 0; index < TRAILER_MAGIC_BYTES.length; index += 1) {
202
+ if (view[footerOffset + 4 + index] !== TRAILER_MAGIC_BYTES[index]) {
203
+ return null;
204
+ }
205
+ }
206
+ return new DataView(
207
+ view.buffer,
208
+ view.byteOffset + footerOffset,
209
+ TRAILER_FOOTER_LENGTH,
210
+ ).getUint32(0, true);
211
+ }
212
+
213
+ function encodeFooter(recordCollectionLength) {
214
+ if (
215
+ !Number.isSafeInteger(recordCollectionLength) ||
216
+ recordCollectionLength < 0 ||
217
+ recordCollectionLength > 0xffff_ffff
218
+ ) {
219
+ throw new RangeError("REC trailer length must fit in uint32.");
220
+ }
221
+ const footer = new Uint8Array(TRAILER_FOOTER_LENGTH);
222
+ const view = new DataView(footer.buffer);
223
+ view.setUint32(0, recordCollectionLength, true);
224
+ footer.set(TRAILER_MAGIC_BYTES, 4);
225
+ return footer;
226
+ }
227
+
228
+ function toBase32Lower(bytes) {
229
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
230
+ let bits = 0;
231
+ let value = 0;
232
+ let out = "";
233
+ for (const byte of toUint8Array(bytes)) {
234
+ value = (value << 8) | byte;
235
+ bits += 8;
236
+ while (bits >= 5) {
237
+ out += alphabet[(value >>> (bits - 5)) & 31];
238
+ bits -= 5;
239
+ }
240
+ }
241
+ if (bits > 0) {
242
+ out += alphabet[(value << (5 - bits)) & 31];
243
+ }
244
+ return out;
245
+ }
246
+
247
+ export async function createCidV1Raw(payloadBytes) {
248
+ const digest = await sha256Bytes(payloadBytes);
249
+ const cidBytes = concatBytes([
250
+ Uint8Array.of(0x01), // cidv1
251
+ Uint8Array.of(0x55), // raw
252
+ Uint8Array.of(0x12, digest.length), // sha2-256 multihash
253
+ digest,
254
+ ]);
255
+ return `b${toBase32Lower(cidBytes)}`;
256
+ }
257
+
258
+ export function encodeEncRecord(record = {}) {
259
+ const builder = new flatbuffers.Builder(256);
260
+ const table = encTableFromObject(record);
261
+ const root = table.pack(builder);
262
+ ENC.finishENCBuffer(builder, root);
263
+ return builder.asUint8Array();
264
+ }
265
+
266
+ export function decodeEncRecord(bytes) {
267
+ const bb = new flatbuffers.ByteBuffer(toUint8Array(bytes));
268
+ if (!ENC.bufferHasIdentifier(bb)) {
269
+ throw new Error("ENC record is missing the $ENC file identifier.");
270
+ }
271
+ const record = ENC.getRootAsENC(bb).unpack();
272
+ return normalizeEncTable(record);
273
+ }
274
+
275
+ export function encodePnmRecord(record = {}) {
276
+ const builder = new flatbuffers.Builder(256);
277
+ const table = pnmTableFromObject(record);
278
+ const root = table.pack(builder);
279
+ PNM.finishPNMBuffer(builder, root);
280
+ return builder.asUint8Array();
281
+ }
282
+
283
+ export function decodePnmRecord(bytes) {
284
+ const bb = new flatbuffers.ByteBuffer(toUint8Array(bytes));
285
+ if (!PNM.bufferHasIdentifier(bb)) {
286
+ throw new Error("PNM record is missing the $PNM file identifier.");
287
+ }
288
+ const record = PNM.getRootAsPNM(bb).unpack();
289
+ return normalizePnmTable(record);
290
+ }
291
+
292
+ export function encodePublicationRecordCollection(options = {}) {
293
+ const records = [];
294
+ if (options.enc) {
295
+ records.push(new RecordT(RECORD_TYPE_BY_STANDARD.ENC, encTableFromObject(options.enc), "ENC"));
296
+ }
297
+ if (options.pnm) {
298
+ records.push(new RecordT(RECORD_TYPE_BY_STANDARD.PNM, pnmTableFromObject(options.pnm), "PNM"));
299
+ }
300
+ if (records.length === 0) {
301
+ throw new Error("At least one ENC or PNM record is required.");
302
+ }
303
+ const builder = new flatbuffers.Builder(512);
304
+ const root = new RECT(
305
+ normalizeStringField(options.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
306
+ records,
307
+ ).pack(builder);
308
+ REC.finishRECBuffer(builder, root);
309
+ return builder.asUint8Array();
310
+ }
311
+
312
+ export function decodePublicationRecordCollection(bytes) {
313
+ const buffer = toUint8Array(bytes);
314
+ const bb = new flatbuffers.ByteBuffer(buffer);
315
+ if (!REC.bufferHasIdentifier(bb)) {
316
+ throw new Error("REC trailer is missing the $REC file identifier.");
317
+ }
318
+ const collection = REC.getRootAsREC(bb).unpack();
319
+ const records = [];
320
+ let enc = null;
321
+ let pnm = null;
322
+ for (const unpackedRecord of Array.isArray(collection.RECORDS) ? collection.RECORDS : []) {
323
+ const standard =
324
+ normalizeStringField(unpackedRecord.standard) ??
325
+ STANDARD_BY_RECORD_TYPE[unpackedRecord.value_type] ??
326
+ null;
327
+ if (standard === "ENC") {
328
+ enc = normalizeEncTable(unpackedRecord.value);
329
+ } else if (standard === "PNM") {
330
+ pnm = normalizePnmTable(unpackedRecord.value);
331
+ }
332
+ records.push({
333
+ standard,
334
+ recordType: unpackedRecord.value_type,
335
+ value:
336
+ standard === "ENC"
337
+ ? enc
338
+ : standard === "PNM"
339
+ ? pnm
340
+ : unpackedRecord.value,
341
+ });
342
+ }
343
+ return {
344
+ version: normalizeStringField(collection.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
345
+ records,
346
+ enc,
347
+ pnm,
348
+ recordCollectionBytes: buffer,
349
+ };
350
+ }
351
+
352
+ export function appendPublicationRecordCollection(
353
+ payloadBytes,
354
+ recordCollectionBytes,
355
+ ) {
356
+ const payload = toUint8Array(payloadBytes);
357
+ const recordCollection = toUint8Array(recordCollectionBytes);
358
+ return concatBytes([
359
+ payload,
360
+ recordCollection,
361
+ encodeFooter(recordCollection.length),
362
+ ]);
363
+ }
364
+
365
+ export function stripPublicationRecordCollection(bytes) {
366
+ const parsed = extractPublicationRecordCollection(bytes);
367
+ return parsed?.payloadBytes ?? toUint8Array(bytes);
368
+ }
369
+
370
+ export function extractPublicationRecordCollection(bytes) {
371
+ const buffer = toUint8Array(bytes);
372
+ const recordCollectionLength = readFooterLength(buffer);
373
+ if (recordCollectionLength === null) {
374
+ return null;
375
+ }
376
+ const footerOffset = buffer.length - TRAILER_FOOTER_LENGTH;
377
+ const recordCollectionOffset = footerOffset - recordCollectionLength;
378
+ if (recordCollectionOffset < 0) {
379
+ return null;
380
+ }
381
+ const recordCollectionBytes = buffer.subarray(
382
+ recordCollectionOffset,
383
+ footerOffset,
384
+ );
385
+ try {
386
+ const decoded = decodePublicationRecordCollection(recordCollectionBytes);
387
+ return {
388
+ ...decoded,
389
+ payloadBytes: buffer.subarray(0, recordCollectionOffset),
390
+ protectedBytes: buffer,
391
+ footerBytes: buffer.subarray(footerOffset),
392
+ footerMagic: TRAILER_MAGIC_TEXT,
393
+ recordCollectionLength,
394
+ };
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+
400
+ export async function createPublicationNotice(options = {}) {
401
+ const payloadBytes = toUint8Array(options.payloadBytes);
402
+ const cid = normalizeStringField(options.cid) ?? (await createCidV1Raw(payloadBytes));
403
+ const publishTimestamp =
404
+ normalizeStringField(options.publishTimestamp) ??
405
+ new Date(
406
+ Number.isFinite(options.publishTimestampMs)
407
+ ? options.publishTimestampMs
408
+ : Date.now(),
409
+ ).toISOString();
410
+ const fileName =
411
+ normalizeStringField(options.fileName) ??
412
+ normalizeStringField(options.artifactId) ??
413
+ "module.wasm";
414
+ const fileId =
415
+ normalizeStringField(options.fileId) ??
416
+ normalizeStringField(options.programId) ??
417
+ normalizeStringField(options.artifactId) ??
418
+ "module";
419
+ const multiformatAddress =
420
+ normalizeStringField(options.multiformatAddress) ?? `/ipfs/${cid}`;
421
+
422
+ let signature = normalizeStringField(options.signature);
423
+ let timestampSignature = normalizeStringField(options.timestampSignature);
424
+ let signatureType = normalizeStringField(options.signatureType);
425
+ let timestampSignatureType = normalizeStringField(options.timestampSignatureType);
426
+ if (options.signer && typeof options.signer.sign === "function") {
427
+ signature = bytesToHex(await options.signer.sign(textEncoder.encode(cid)));
428
+ timestampSignature = bytesToHex(
429
+ await options.signer.sign(textEncoder.encode(publishTimestamp)),
430
+ );
431
+ signatureType =
432
+ signatureType ??
433
+ normalizeStringField(options.signer.algorithm) ??
434
+ "unknown";
435
+ timestampSignatureType =
436
+ timestampSignatureType ??
437
+ normalizeStringField(options.signer.algorithm) ??
438
+ "unknown";
439
+ }
440
+
441
+ return {
442
+ multiformatAddress,
443
+ publishTimestamp,
444
+ cid,
445
+ fileName,
446
+ fileId,
447
+ signature,
448
+ timestampSignature,
449
+ signatureType,
450
+ timestampSignatureType,
451
+ };
452
+ }
453
+
454
+ export function createEncryptedEnvelopePayload(options = {}) {
455
+ const protectedBlob = toUint8Array(options.protectedBlobBytes);
456
+ const parsed =
457
+ options.parsedProtectedBlob ?? extractPublicationRecordCollection(protectedBlob);
458
+ const enc = options.enc ?? parsed?.enc ?? null;
459
+ const envelope = {
460
+ version: Number(options.version ?? 2),
461
+ scheme:
462
+ normalizeStringField(options.scheme) ?? "x25519-hkdf-aes-256-ctr-rec",
463
+ context: normalizeStringField(options.context ?? enc?.context) ?? "",
464
+ protectedBlobBase64: bytesToBase64(protectedBlob),
465
+ recordCollectionBase64: parsed
466
+ ? bytesToBase64(parsed.recordCollectionBytes)
467
+ : null,
468
+ ciphertextBase64: parsed ? bytesToBase64(parsed.payloadBytes) : null,
469
+ };
470
+ if (enc?.ephemeralPublicKey) {
471
+ envelope.senderPublicKeyBase64 = bytesToBase64(enc.ephemeralPublicKey);
472
+ }
473
+ if (enc?.nonceStart) {
474
+ envelope.nonceStartBase64 = bytesToBase64(enc.nonceStart);
475
+ }
476
+ if (enc?.recipientKeyId) {
477
+ envelope.recipientKeyIdBase64 = bytesToBase64(enc.recipientKeyId);
478
+ }
479
+ if (enc) {
480
+ envelope.encRecordBase64 = bytesToBase64(encodeEncRecord(enc));
481
+ }
482
+ if (parsed?.pnm) {
483
+ envelope.pnmRecordBase64 = bytesToBase64(encodePnmRecord(parsed.pnm));
484
+ }
485
+ return envelope;
486
+ }
487
+
488
+ export function decodeProtectedBlobBase64(base64) {
489
+ const bytes = base64ToBytes(base64);
490
+ return extractPublicationRecordCollection(bytes);
491
+ }
492
+
493
+ export { TRAILER_MAGIC_TEXT, TRAILER_FOOTER_LENGTH };
@@ -116,3 +116,46 @@ export async function aesGcmDecrypt(key, ciphertext, tag, iv, aad = null) {
116
116
  aad ? toUint8Array(aad) : undefined,
117
117
  );
118
118
  }
119
+
120
+ function normalizeCtrIv(nonceStart) {
121
+ const nonce = toUint8Array(nonceStart);
122
+ if (nonce.length !== 12) {
123
+ throw new Error("AES-256-CTR expects a 12-byte NONCE_START value.");
124
+ }
125
+ const iv = new Uint8Array(16);
126
+ iv.set(nonce, 0);
127
+ return iv;
128
+ }
129
+
130
+ async function getAesCtrApi() {
131
+ const wallet = await getWasmWallet();
132
+ const aesCtr = wallet?.aesCtr;
133
+ if (
134
+ !aesCtr ||
135
+ typeof aesCtr.encrypt !== "function" ||
136
+ typeof aesCtr.decrypt !== "function"
137
+ ) {
138
+ throw new Error(
139
+ "hd-wallet-wasm aesCtr API is unavailable; cannot process ENC payloads.",
140
+ );
141
+ }
142
+ return aesCtr;
143
+ }
144
+
145
+ export async function aesCtrEncrypt(key, plaintext, nonceStart) {
146
+ const aesCtr = await getAesCtrApi();
147
+ return aesCtr.encrypt(
148
+ toUint8Array(key),
149
+ toUint8Array(plaintext),
150
+ normalizeCtrIv(nonceStart),
151
+ );
152
+ }
153
+
154
+ export async function aesCtrDecrypt(key, ciphertext, nonceStart) {
155
+ const aesCtr = await getAesCtrApi();
156
+ return aesCtr.decrypt(
157
+ toUint8Array(key),
158
+ toUint8Array(ciphertext),
159
+ normalizeCtrIv(nonceStart),
160
+ );
161
+ }