UNPKG

c2pa-node

Version:
250 lines (249 loc) 9.87 kB
"use strict"; /** * Copyright 2023 Adobe * All Rights Reserved. * * NOTICE: Adobe permits you to use, modify, and distribute this file in * accordance with the terms of the Adobe license agreement accompanying * it. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultSignOptions = void 0; exports.resolveManifest = resolveManifest; exports.read = read; exports.createSign = createSign; exports.createIngredientFunction = createIngredientFunction; const error_1 = require("./lib/error"); const hash_1 = require("./lib/hash"); const thumbnail_1 = require("./lib/thumbnail"); const C2PA_LIBRARY_PATH = process.env.C2PA_LIBRARY_PATH; const bindings = require(C2PA_LIBRARY_PATH ?? '../generated/c2pa.node'); const missingErrors = [ // No embedded or remote provenance found in the asset 'C2pa(ProvenanceMissing)', // JUMBF not found 'C2pa(JumbfNotFound)', ]; function parseSignatureInfo(manifest) { const info = manifest.signature_info; if (!info) { return {}; } return { signature_info: { ...info, timeObject: typeof info.time === 'string' ? new Date(info.time) : info.time, }, }; } function createIngredientResolver(manifestStore, resourceStore) { return (ingredient) => { const relatedManifest = ingredient.active_manifest; const thumbnailIdentifier = ingredient.thumbnail?.identifier; const thumbnailResource = thumbnailIdentifier ? resourceStore[thumbnailIdentifier] : null; return { ...ingredient, manifest: relatedManifest ? manifestStore.manifests[relatedManifest] : null, thumbnail: thumbnailResource ? { format: ingredient.thumbnail?.format ?? '', data: Buffer.from(thumbnailResource.buffer), } : null, }; }; } function resolveManifest(manifestStore, manifest, resourceStore) { const thumbnailIdentifier = manifest.thumbnail?.identifier; const thumbnailResource = thumbnailIdentifier ? resourceStore[thumbnailIdentifier] : null; const ingredientResolver = createIngredientResolver(manifestStore, resourceStore); return { ...manifest, ...parseSignatureInfo(manifest), ingredients: (manifest.ingredients ?? []).map(ingredientResolver), thumbnail: thumbnailResource ? { format: manifest.thumbnail?.format ?? '', data: Buffer.from(thumbnailResource.buffer), } : null, }; } /** * Reads C2PA data from an asset * @param asset * @returns A promise containing C2PA data, if present */ async function read(asset) { try { const result = await bindings.read(asset); const manifestStore = JSON.parse(result.manifest_store); const resourceStore = result.resource_store; const activeManifestLabel = manifestStore.active_manifest; const manifests = Object.keys(manifestStore.manifests).reduce((acc, label) => { const manifest = manifestStore.manifests[label]; return { ...acc, [label]: resolveManifest(manifestStore, manifest, resourceStore[label]), }; }, {}); return { active_manifest: activeManifestLabel ? manifests[activeManifestLabel] : null, manifests, validation_status: manifestStore.validation_status ?? [], }; } catch (err) { if (missingErrors.some((test) => test === err?.name)) { return null; } throw err; } } exports.defaultSignOptions = { embed: true, }; function createSign(globalOptions) { return { /** * Signs a C2PA manifest and optionally embeds it in the asset * @param props * @returns */ async sign(props) { const { asset, manifest, thumbnail, signer: customSigner, options, } = props; const signOptions = Object.assign({}, exports.defaultSignOptions, options); const signer = customSigner ?? globalOptions.signer; const memoryFileTypes = ['image/jpeg', 'image/png']; if (!signer) { throw new error_1.MissingSignerError(); } if (!signOptions.embed && !signOptions.remoteManifestUrl) { throw new error_1.InvalidStorageOptionsError(); } if ('buffer' in asset && !memoryFileTypes.includes(asset.mimeType)) { throw new Error(`Only ${memoryFileTypes.join(', ')} files can be signed using a memory buffer.`); } try { const signOpts = { ...signOptions, signer }; if (!manifest.definition.thumbnail) { const thumbnailInput = 'buffer' in asset ? asset.buffer : asset.path; const thumbnailAsset = // Use thumbnail if provided thumbnail || // Otherwise generate one if configured to do so (globalOptions.thumbnail && thumbnail !== false ? await (0, thumbnail_1.createThumbnail)(thumbnailInput, globalOptions.thumbnail) : null); if (thumbnailAsset) { await manifest.addThumbnail(thumbnailAsset); } } if ('buffer' in asset) { const { mimeType } = asset; const assetSignOpts = { ...signOpts, format: mimeType }; const result = await bindings.sign(manifest.asSendable(), asset, assetSignOpts); const { assetBuffer: signedAssetBuffer, manifest: signedManifest } = result; const signedAsset = { buffer: Buffer.from(signedAssetBuffer), mimeType, }; return { signedAsset, signedManifest: signedManifest ? Buffer.from(signedManifest) : undefined, }; } else { const { mimeType } = asset; const { outputPath } = await bindings.sign(manifest.asSendable(), asset, signOpts); return { signedAsset: { path: outputPath, mimeType, }, }; } } catch (err) { throw new error_1.SigningError({ cause: err }); } }, /** * Signs the bytes of a C2PA claim * @param props * @returns The CBOR bytes of COSE_Sign1 (signature box of JUMBF) */ async signClaimBytes({ claim, reserveSize, signer, }) { try { const result = await bindings.sign_claim_bytes(claim, reserveSize, signer); return Buffer.from(result); } catch (err) { throw new error_1.SigningError({ cause: err }); } }, }; } function createIngredientFunction(options) { /** * @notExported * Creates a storable ingredient from an asset. * * This allows ingredient data to be extracted, optionally stored, and passed in during signing at a later time if needed. */ return async function createIngredient({ asset, title, thumbnail, hash: suppliedHash, }) { try { let serializedIngredient; let existingResources; const hash = suppliedHash ?? (await (0, hash_1.labeledSha)(asset, options.ingredientHashAlgorithm)); ({ ingredient: serializedIngredient, resources: existingResources } = await bindings.create_ingredient(asset)); const ingredient = JSON.parse(serializedIngredient); // Separate resources out into their own object so they can be stored more easily const resources = Object.keys(existingResources).reduce((acc, identifier) => { return { ...acc, [identifier]: Buffer.from(existingResources[identifier]), }; }, {}); // Clear out resources since we are not using this field ingredient.resources = undefined; ingredient.title = title; ingredient.hash = hash; // Generate a thumbnail if one doesn't exist on the ingredient's manifest if (!ingredient.thumbnail) { const thumbnailInput = 'buffer' in asset ? asset.buffer : asset.path; const thumbnailAsset = // Use thumbnail if provided thumbnail || // Otherwise generate one if configured to do so (options.thumbnail && thumbnail !== false ? await (0, thumbnail_1.createThumbnail)(thumbnailInput ?? asset, options.thumbnail) : null); if (thumbnailAsset) { const resourceRef = await (0, hash_1.getResourceReference)(thumbnailAsset, ingredient.instance_id); ingredient.thumbnail = resourceRef; resources[resourceRef.identifier] = thumbnailAsset.buffer; } } return { ingredient, resources, }; } catch (err) { throw new error_1.CreateIngredientError({ cause: err }); } }; }