11import { canonicalBytes } from "../auth/canonicalize.js" ;
2+ import * as flatbuffers from "flatbuffers" ;
3+ import { KMF , keyMaterialAlgorithm , keyMaterialEncoding , keyMaterialRole } from "spacedatastandards.org/lib/js/KMF/main.js" ;
24import {
35 base64ToBytes ,
46 bytesToBase64 ,
7+ bytesToHex ,
58 hexToBytes ,
69 toUint8Array ,
710} from "../utils/encoding.js" ;
@@ -53,6 +56,130 @@ async function deriveAesKey(sharedSecret, salt, context) {
5356 ) ;
5457}
5558
59+ function normalizeOptionalBytes ( value ) {
60+ if ( value === null || value === undefined ) {
61+ return new Uint8Array ( 0 ) ;
62+ }
63+ if ( typeof value === "string" ) {
64+ return hexToBytes ( value ) ;
65+ }
66+ return toUint8Array ( value ) ;
67+ }
68+
69+ function normalizeRequiredString ( value , name ) {
70+ const normalized = String ( value ?? "" ) . trim ( ) ;
71+ if ( ! normalized ) {
72+ throw new Error ( `${ name } is required.` ) ;
73+ }
74+ return normalized ;
75+ }
76+
77+ function normalizeArtifactBinding ( artifact = { } ) {
78+ const listingId = normalizeRequiredString ( artifact . listingId , "artifact.listingId" ) ;
79+ const moduleId = normalizeRequiredString ( artifact . moduleId , "artifact.moduleId" ) ;
80+ const version = normalizeRequiredString ( artifact . version , "artifact.version" ) ;
81+ const encryptedCid = normalizeRequiredString (
82+ artifact . encryptedCid ,
83+ "artifact.encryptedCid" ,
84+ ) ;
85+ const providerId = normalizeRequiredString ( artifact . providerId , "artifact.providerId" ) ;
86+ const policyId = normalizeRequiredString ( artifact . policyId , "artifact.policyId" ) ;
87+ const keyEpoch = normalizeRequiredString ( artifact . keyEpoch , "artifact.keyEpoch" ) ;
88+ const contentKeyId =
89+ String ( artifact . contentKeyId ?? "" ) . trim ( ) ||
90+ `${ listingId } :${ moduleId } :${ version } :${ keyEpoch } ` ;
91+ const encryptedHash = normalizeOptionalBytes ( artifact . encryptedHash ) ;
92+ if ( encryptedHash . length !== 32 ) {
93+ throw new Error ( "artifact.encryptedHash must be 32 bytes." ) ;
94+ }
95+ const manifestHash = normalizeOptionalBytes ( artifact . manifestHash ) ;
96+ if ( manifestHash . length !== 32 ) {
97+ throw new Error ( "artifact.manifestHash must be 32 bytes." ) ;
98+ }
99+ return {
100+ listingId,
101+ moduleId,
102+ version,
103+ encryptedCid,
104+ encryptedHash : bytesToHex ( encryptedHash ) ,
105+ providerId,
106+ policyId,
107+ keyEpoch,
108+ manifestHash : bytesToHex ( manifestHash ) ,
109+ contentKeyId,
110+ } ;
111+ }
112+
113+ export function createMarketplaceContentAad ( artifact = { } ) {
114+ const binding = normalizeArtifactBinding ( artifact ) ;
115+ return canonicalBytes ( {
116+ encryptedCid : binding . encryptedCid ,
117+ encryptedHash : binding . encryptedHash ,
118+ keyEpoch : binding . keyEpoch ,
119+ listingId : binding . listingId ,
120+ manifestHash : binding . manifestHash ,
121+ moduleId : binding . moduleId ,
122+ policyId : binding . policyId ,
123+ providerId : binding . providerId ,
124+ version : binding . version ,
125+ } ) ;
126+ }
127+
128+ function createWrapAad ( { artifact, recipient, algorithm } ) {
129+ return canonicalBytes ( {
130+ algorithm,
131+ contentKeyId : artifact . contentKeyId ,
132+ encryptedCid : artifact . encryptedCid ,
133+ expiresAtMs : Number ( recipient . expiresAtMs ?? 0 ) ,
134+ grantId : normalizeRequiredString ( recipient . grantId , "recipient.grantId" ) ,
135+ keyEpoch : artifact . keyEpoch ,
136+ providerId : artifact . providerId ,
137+ recipientKeyId : normalizeRequiredString (
138+ recipient . recipientKeyId ,
139+ "recipient.recipientKeyId" ,
140+ ) ,
141+ scope : normalizeRequiredString ( recipient . scope , "recipient.scope" ) ,
142+ } ) ;
143+ }
144+
145+ function encodeContentKeyMaterial ( { keyId, keyBytes, expiresAtMs } ) {
146+ const builder = new flatbuffers . Builder ( 256 ) ;
147+ const keyIdOffset = builder . createString ( keyId ) ;
148+ const keyBytesOffset = KMF . createKeyBytesVector ( builder , keyBytes ) ;
149+ const root = KMF . createKMF (
150+ builder ,
151+ keyIdOffset ,
152+ keyMaterialRole . PublicationContent ,
153+ keyMaterialAlgorithm . Aes256Gcm ,
154+ keyMaterialEncoding . RawBytes ,
155+ keyBytesOffset ,
156+ 1 ,
157+ BigInt ( Number ( expiresAtMs ?? 0 ) ) ,
158+ ) ;
159+ KMF . finishKMFBuffer ( builder , root ) ;
160+ return builder . asUint8Array ( ) ;
161+ }
162+
163+ function decodeContentKeyMaterial ( bytes ) {
164+ const payload = toUint8Array ( bytes ) ;
165+ const bb = new flatbuffers . ByteBuffer ( payload ) ;
166+ if ( ! KMF . bufferHasIdentifier ( bb ) ) {
167+ throw new Error ( "Wrapped marketplace content key payload is not a KMF record." ) ;
168+ }
169+ const kmf = KMF . getRootAsKMF ( bb ) ;
170+ if ( kmf . ROLE ( ) !== keyMaterialRole . PublicationContent ) {
171+ throw new Error ( "Wrapped marketplace key material is not a publication content key." ) ;
172+ }
173+ if ( kmf . ALGORITHM ( ) !== keyMaterialAlgorithm . Aes256Gcm ) {
174+ throw new Error ( "Wrapped marketplace key material is not an AES-256-GCM key." ) ;
175+ }
176+ const keyBytes = kmf . keyBytesArray ( ) ;
177+ if ( ! keyBytes || keyBytes . length !== 32 ) {
178+ throw new Error ( "Wrapped marketplace content key must be 32 bytes." ) ;
179+ }
180+ return new Uint8Array ( keyBytes ) ;
181+ }
182+
56183export async function generateX25519Keypair ( ) {
57184 const privateKey = await randomBytes ( 32 ) ;
58185 const publicKey = await x25519PublicKey ( privateKey ) ;
@@ -62,6 +189,121 @@ export async function generateX25519Keypair() {
62189 } ;
63190}
64191
192+ export async function protectMarketplaceContent ( {
193+ plaintext,
194+ artifact,
195+ recipients = [ ] ,
196+ contentKey = null ,
197+ contentNonce = null ,
198+ providerWrapKeyPair = null ,
199+ wrapNonce = null ,
200+ } = { } ) {
201+ if ( ! Array . isArray ( recipients ) || recipients . length === 0 ) {
202+ throw new Error ( "protectMarketplaceContent requires at least one recipient." ) ;
203+ }
204+ const binding = normalizeArtifactBinding ( artifact ) ;
205+ const keyBytes = contentKey ? toUint8Array ( contentKey ) : await randomBytes ( 32 ) ;
206+ if ( keyBytes . length !== 32 ) {
207+ throw new Error ( "Marketplace content keys must be 32 bytes." ) ;
208+ }
209+ const nonce = contentNonce ? toUint8Array ( contentNonce ) : await randomBytes ( 12 ) ;
210+ if ( nonce . length !== 12 ) {
211+ throw new Error ( "AES-256-GCM content encryption requires a 12-byte nonce." ) ;
212+ }
213+ const aad = createMarketplaceContentAad ( binding ) ;
214+ const encrypted = await aesGcmEncrypt (
215+ keyBytes ,
216+ toUint8Array ( plaintext ) ,
217+ nonce ,
218+ aad ,
219+ ) ;
220+ const wrappedKeys = [ ] ;
221+ for ( const recipient of recipients ) {
222+ const sender = providerWrapKeyPair ?? ( await generateX25519Keypair ( ) ) ;
223+ const recipientPublicKey = normalizePublicKey ( recipient . publicKey ) ;
224+ const wrapIv = wrapNonce ? toUint8Array ( wrapNonce ) : await randomBytes ( 12 ) ;
225+ if ( wrapIv . length !== 12 ) {
226+ throw new Error ( "AES-256-GCM key wrapping requires a 12-byte nonce." ) ;
227+ }
228+ const wrapAad = createWrapAad ( {
229+ artifact : binding ,
230+ recipient,
231+ algorithm : "X25519-HKDF-SHA256-AES-256-GCM-KMF" ,
232+ } ) ;
233+ const sharedSecret = await deriveSharedSecret (
234+ sender . privateKey ,
235+ recipientPublicKey ,
236+ ) ;
237+ const wrapContext = `marketplace-content-key:${ binding . providerId } :${ binding . contentKeyId } :${ recipient . recipientKeyId } ` ;
238+ const wrapKey = await deriveAesKey ( sharedSecret , new Uint8Array ( 0 ) , wrapContext ) ;
239+ const keyMaterial = encodeContentKeyMaterial ( {
240+ keyId : binding . contentKeyId ,
241+ keyBytes,
242+ expiresAtMs : recipient . expiresAtMs ,
243+ } ) ;
244+ const wrapped = await aesGcmEncrypt ( wrapKey , keyMaterial , wrapIv , wrapAad ) ;
245+ wrappedKeys . push ( {
246+ algorithm : "X25519-HKDF-SHA256-AES-256-GCM-KMF" ,
247+ contentKeyId : binding . contentKeyId ,
248+ recipientPeerId : normalizeRequiredString (
249+ recipient . recipientPeerId ,
250+ "recipient.recipientPeerId" ,
251+ ) ,
252+ recipientKeyId : normalizeRequiredString (
253+ recipient . recipientKeyId ,
254+ "recipient.recipientKeyId" ,
255+ ) ,
256+ providerId : binding . providerId ,
257+ encryptedCid : binding . encryptedCid ,
258+ grantId : normalizeRequiredString ( recipient . grantId , "recipient.grantId" ) ,
259+ scope : normalizeRequiredString ( recipient . scope , "recipient.scope" ) ,
260+ expiresAtMs : Number ( recipient . expiresAtMs ?? 0 ) ,
261+ providerEphemeralPublicKey : sender . publicKey ,
262+ nonce : wrapIv ,
263+ aad : wrapAad ,
264+ ciphertext : wrapped . ciphertext ,
265+ tag : wrapped . tag ,
266+ } ) ;
267+ }
268+ return {
269+ algorithm : "AES-256-GCM" ,
270+ contentKeyId : binding . contentKeyId ,
271+ artifact : binding ,
272+ aad,
273+ encryptedPayload : {
274+ algorithm : "AES-256-GCM" ,
275+ nonce,
276+ aad,
277+ ciphertext : encrypted . ciphertext ,
278+ tag : encrypted . tag ,
279+ } ,
280+ wrappedKeys,
281+ } ;
282+ }
283+
284+ export async function decryptMarketplaceContentKeyWrap ( {
285+ wrap,
286+ recipientPrivateKey,
287+ } = { } ) {
288+ if ( ! wrap ) {
289+ throw new Error ( "decryptMarketplaceContentKeyWrap requires wrap." ) ;
290+ }
291+ const sharedSecret = await deriveSharedSecret (
292+ recipientPrivateKey ,
293+ wrap . providerEphemeralPublicKey ,
294+ ) ;
295+ const wrapContext = `marketplace-content-key:${ wrap . providerId } :${ wrap . contentKeyId } :${ wrap . recipientKeyId } ` ;
296+ const wrapKey = await deriveAesKey ( sharedSecret , new Uint8Array ( 0 ) , wrapContext ) ;
297+ const keyMaterial = await aesGcmDecrypt (
298+ wrapKey ,
299+ wrap . ciphertext ,
300+ wrap . tag ,
301+ wrap . nonce ,
302+ wrap . aad ,
303+ ) ;
304+ return decodeContentKeyMaterial ( keyMaterial ) ;
305+ }
306+
65307async function encryptBytesLegacy ( {
66308 plaintext,
67309 recipientPublicKey,
0 commit comments