diff --git a/lib/tdf3/index.ts b/lib/tdf3/index.ts index dc9fe0e0..d33f868d 100644 --- a/lib/tdf3/index.ts +++ b/lib/tdf3/index.ts @@ -20,11 +20,8 @@ import { type PemKeyPair, } from './src/crypto/declarations.js'; import { Client, Errors, TDF3Client } from './src/index.js'; -import { - type KeyInfo, - SplitKey, - type EncryptionInformation, -} from './src/models/encryption-information.js'; +import { type EncryptionInformation } from './src/models/encryption-information.js'; +import { type KeyInfo, SplitKey } from './src/splits.js'; import { AuthProvider, type HttpMethod, HttpRequest, withHeaders } from '../src/auth/auth.js'; import { AesGcmCipher } from './src/ciphers/aes-gcm-cipher.js'; import { diff --git a/lib/tdf3/src/client/builders.ts b/lib/tdf3/src/client/builders.ts index a426634a..5f7550c0 100644 --- a/lib/tdf3/src/client/builders.ts +++ b/lib/tdf3/src/client/builders.ts @@ -1,8 +1,8 @@ import { validateAttribute, validateAttributeObject } from './validation.js'; -import { AttributeObject, KeyInfo, Policy } from '../models/index.js'; +import { AttributeObject, Policy } from '../models/index.js'; import { type Metadata } from '../tdf.js'; import { Binary } from '../binary.js'; - +import { KeyInfo } from '../splits.js'; import { ConfigurationError } from '../../../src/errors.js'; import { PemKeyPair } from '../crypto/declarations.js'; import { DecoratedReadableStream } from './DecoratedReadableStream.js'; diff --git a/lib/tdf3/src/client/index.ts b/lib/tdf3/src/client/index.ts index d7dd2cc5..8cbe13b9 100644 --- a/lib/tdf3/src/client/index.ts +++ b/lib/tdf3/src/client/index.ts @@ -48,16 +48,12 @@ import { Binary } from '../binary.js'; import { AesGcmCipher } from '../ciphers/aes-gcm-cipher.js'; import { toCryptoKeyPair } from '../crypto/crypto-utils.js'; import * as defaultCryptoService from '../crypto/index.js'; -import { - type AttributeObject, - type KeyAccessType, - type Policy, - SplitKey, -} from '../models/index.js'; +import { type AttributeObject, type KeyAccessType, type Policy } from '../models/index.js'; import { plan } from '../../../src/policy/granter.js'; import { attributeFQNsAsValues } from '../../../src/policy/api.js'; import { type Value } from '../../../src/policy/attributes.js'; import { type Chunker, fromBuffer, fromSource } from '../../../src/seekable.js'; +import { SplitKey } from '../splits.js'; const GLOBAL_BYTE_LIMIT = 64 * 1000 * 1000 * 1000; // 64 GB, see WS-9363. diff --git a/lib/tdf3/src/kao-builders.ts b/lib/tdf3/src/kao-builders.ts new file mode 100644 index 00000000..0599d9fa --- /dev/null +++ b/lib/tdf3/src/kao-builders.ts @@ -0,0 +1,137 @@ +import { base64, hex } from '../../src/encodings/index.js'; +import { generateRandomNumber } from '../../src/nanotdf-crypto/generateRandomNumber.js'; +import { keyAgreement } from '../../src/nanotdf-crypto/keyAgreement.js'; +import { pemPublicToCrypto } from '../../src/nanotdf-crypto/pemPublicToCrypto.js'; +import { cryptoPublicToPem } from '../../src/utils.js'; +import { Binary } from './binary.js'; +import * as cryptoService from './crypto/index.js'; +import { ztdfSalt } from './crypto/salt.js'; +import { type KeyAccessObject, type Policy } from './models/index.js'; + +export const schemaVersion = '1.0'; + +export class ECWrapped { + readonly type = 'ec-wrapped'; + readonly ephemeralKeyPair: Promise; + keyAccessObject?: KeyAccessObject; + + constructor( + public readonly url: string, + public readonly kid: string | undefined, + public readonly publicKey: string, + public readonly metadata: unknown, + public readonly sid?: string + ) { + this.ephemeralKeyPair = crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256', + }, + false, + ['deriveBits', 'deriveKey'] + ); + } + + async write( + policy: Policy, + dek: Uint8Array, + encryptedMetadataStr: string + ): Promise { + const policyStr = JSON.stringify(policy); + const [ek, clientPublicKey] = await Promise.all([ + this.ephemeralKeyPair, + pemPublicToCrypto(this.publicKey), + ]); + const kek = await keyAgreement(ek.privateKey, clientPublicKey, { + hkdfSalt: await ztdfSalt, + hkdfHash: 'SHA-256', + }); + const iv = generateRandomNumber(12); + const cek = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, kek, dek); + const entityWrappedKey = new Uint8Array(iv.length + cek.byteLength); + entityWrappedKey.set(iv); + entityWrappedKey.set(new Uint8Array(cek), iv.length); + + const policyBinding = await cryptoService.hmac( + hex.encodeArrayBuffer(dek), + base64.encode(policyStr) + ); + + const ephemeralPublicKeyPEM = await cryptoPublicToPem(ek.publicKey); + const kao: KeyAccessObject = { + type: 'ec-wrapped', + url: this.url, + protocol: 'kas', + wrappedKey: base64.encodeArrayBuffer(entityWrappedKey), + encryptedMetadata: base64.encode(encryptedMetadataStr), + policyBinding: { + alg: 'HS256', + hash: base64.encode(policyBinding), + }, + schemaVersion, + ephemeralPublicKey: ephemeralPublicKeyPEM, + }; + if (this.kid) { + kao.kid = this.kid; + } + if (this.sid?.length) { + kao.sid = this.sid; + } + this.keyAccessObject = kao; + return kao; + } +} + +export class Wrapped { + readonly type = 'wrapped'; + keyAccessObject?: KeyAccessObject; + + constructor( + public readonly url: string, + public readonly kid: string | undefined, + public readonly publicKey: string, + public readonly metadata: unknown, + public readonly sid?: string + ) {} + + async write( + policy: Policy, + keyBuffer: Uint8Array, + encryptedMetadataStr: string + ): Promise { + const policyStr = JSON.stringify(policy); + const unwrappedKeyBinary = Binary.fromArrayBuffer(keyBuffer.buffer); + const wrappedKeyBinary = await cryptoService.encryptWithPublicKey( + unwrappedKeyBinary, + this.publicKey + ); + + const policyBinding = await cryptoService.hmac( + hex.encodeArrayBuffer(keyBuffer), + base64.encode(policyStr) + ); + + this.keyAccessObject = { + type: 'wrapped', + url: this.url, + protocol: 'kas', + wrappedKey: base64.encode(wrappedKeyBinary.asString()), + encryptedMetadata: base64.encode(encryptedMetadataStr), + policyBinding: { + alg: 'HS256', + hash: base64.encode(policyBinding), + }, + schemaVersion, + }; + if (this.kid) { + this.keyAccessObject.kid = this.kid; + } + if (this.sid?.length) { + this.keyAccessObject.sid = this.sid; + } + + return this.keyAccessObject; + } +} + +export type KeyAccess = ECWrapped | Wrapped; diff --git a/lib/tdf3/src/models/encryption-information.ts b/lib/tdf3/src/models/encryption-information.ts index 6aeca079..30e34ce6 100644 --- a/lib/tdf3/src/models/encryption-information.ts +++ b/lib/tdf3/src/models/encryption-information.ts @@ -1,21 +1,5 @@ -import { keySplit } from '../utils/index.js'; -import { base64, hex } from '../../../src/encodings/index.js'; -import { Binary } from '../binary.js'; -import { type SymmetricCipher } from '../ciphers/symmetric-cipher-base.js'; -import { type KeyAccess, type KeyAccessObject } from './key-access.js'; -import { type Policy } from './policy.js'; -import { - type CryptoService, - type DecryptResult, - type EncryptResult, -} from '../crypto/declarations.js'; +import { type KeyAccessObject } from './key-access.js'; import { IntegrityAlgorithm } from '../tdf.js'; -import { ConfigurationError } from '../../../src/errors.js'; - -export type KeyInfo = { - readonly unwrappedKeyBinary: Binary; - readonly unwrappedKeyIvBinary: Binary; -}; export type Segment = { readonly hash: string; @@ -47,126 +31,3 @@ export type EncryptionInformation = { }; policy: string; }; - -export class SplitKey { - readonly cryptoService: CryptoService; - keyAccess: KeyAccess[]; - - constructor(public readonly cipher: SymmetricCipher) { - this.cryptoService = cipher.cryptoService; - this.keyAccess = []; - } - - async generateKey(): Promise { - const unwrappedKey = await this.cipher.generateKey(); - const unwrappedKeyBinary = Binary.fromString(hex.decode(unwrappedKey)); - const unwrappedKeyIvBinary = await this.generateIvBinary(); - return { unwrappedKeyBinary, unwrappedKeyIvBinary }; - } - - async encrypt( - contentBinary: Binary, - keyBinary: Binary, - ivBinaryOptional?: Binary - ): Promise { - const ivBinary = ivBinaryOptional || (await this.generateIvBinary()); - return this.cipher.encrypt(contentBinary, keyBinary, ivBinary); - } - - async decrypt(content: Uint8Array, keyBinary: Binary): Promise { - return this.cipher.decrypt(content, keyBinary); - } - - async getKeyAccessObjects(policy: Policy, keyInfo: KeyInfo): Promise { - const splitIds = [...new Set(this.keyAccess.map(({ sid }) => sid))].sort((a = '', b = '') => - a.localeCompare(b) - ); - const unwrappedKeySplitBuffers = await keySplit( - new Uint8Array(keyInfo.unwrappedKeyBinary.asByteArray()), - splitIds.length, - this.cryptoService - ); - const splitsByName = Object.fromEntries( - splitIds.map((sid, index) => [sid, unwrappedKeySplitBuffers[index]]) - ); - - const keyAccessObjects = []; - for (const item of this.keyAccess) { - // use the key split to encrypt metadata for each key access object - const unwrappedKeySplitBuffer = splitsByName[item.sid || '']; - const unwrappedKeySplitBinary = Binary.fromArrayBuffer(unwrappedKeySplitBuffer.buffer); - - const metadata = item.metadata || ''; - const metadataStr = ( - typeof metadata === 'object' - ? JSON.stringify(metadata) - : typeof metadata === 'string' - ? metadata - : () => { - throw new ConfigurationError( - "KAO generation failure: metadata isn't a string or object" - ); - } - ) as string; - - const metadataBinary = Binary.fromArrayBuffer(new TextEncoder().encode(metadataStr)); - - const encryptedMetadataResult = await this.encrypt( - metadataBinary, - unwrappedKeySplitBinary, - keyInfo.unwrappedKeyIvBinary - ); - - const encryptedMetadataOb = { - ciphertext: base64.encode(encryptedMetadataResult.payload.asString()), - iv: base64.encode(keyInfo.unwrappedKeyIvBinary.asString()), - }; - - const encryptedMetadataStr = JSON.stringify(encryptedMetadataOb); - const keyAccessObject = await item.write( - policy, - unwrappedKeySplitBuffer, - encryptedMetadataStr - ); - keyAccessObjects.push(keyAccessObject); - } - - return keyAccessObjects; - } - - async generateIvBinary(): Promise { - const iv = await this.cipher.generateInitializationVector(); - return Binary.fromString(hex.decode(iv)); - } - - async write(policy: Policy, keyInfo: KeyInfo): Promise { - const algorithm = this.cipher?.name; - if (!algorithm) { - // Hard coded as part of the cipher object. This should not be reachable. - throw new ConfigurationError('uninitialized cipher type'); - } - const keyAccessObjects = await this.getKeyAccessObjects(policy, keyInfo); - - // For now we're only concerned with a single (first) key access object - const policyForManifest = base64.encode(JSON.stringify(policy)); - - return { - type: 'split', - keyAccess: keyAccessObjects, - method: { - algorithm, - isStreamable: false, - iv: base64.encode(keyInfo.unwrappedKeyIvBinary.asString()), - }, - integrityInformation: { - rootSignature: { - alg: 'HS256', - sig: '', - }, - segmentHashAlg: 'GMAC', - segments: [], - }, - policy: policyForManifest, - }; - } -} diff --git a/lib/tdf3/src/models/key-access.ts b/lib/tdf3/src/models/key-access.ts index 5b30cf7e..ad821c64 100644 --- a/lib/tdf3/src/models/key-access.ts +++ b/lib/tdf3/src/models/key-access.ts @@ -1,143 +1,5 @@ -import { base64, hex } from '../../../src/encodings/index.js'; -import { generateRandomNumber } from '../../../src/nanotdf-crypto/generateRandomNumber.js'; -import { keyAgreement } from '../../../src/nanotdf-crypto/keyAgreement.js'; -import { pemPublicToCrypto } from '../../../src/nanotdf-crypto/pemPublicToCrypto.js'; -import { cryptoPublicToPem } from '../../../src/utils.js'; -import { Binary } from '../binary.js'; -import * as cryptoService from '../crypto/index.js'; -import { ztdfSalt } from '../crypto/salt.js'; -import { Policy } from './policy.js'; - export type KeyAccessType = 'remote' | 'wrapped' | 'ec-wrapped'; -export const schemaVersion = '1.0'; - -export class ECWrapped { - readonly type = 'ec-wrapped'; - readonly ephemeralKeyPair: Promise; - keyAccessObject?: KeyAccessObject; - - constructor( - public readonly url: string, - public readonly kid: string | undefined, - public readonly publicKey: string, - public readonly metadata: unknown, - public readonly sid?: string - ) { - this.ephemeralKeyPair = crypto.subtle.generateKey( - { - name: 'ECDH', - namedCurve: 'P-256', - }, - false, - ['deriveBits', 'deriveKey'] - ); - } - - async write( - policy: Policy, - dek: Uint8Array, - encryptedMetadataStr: string - ): Promise { - const policyStr = JSON.stringify(policy); - const [ek, clientPublicKey] = await Promise.all([ - this.ephemeralKeyPair, - pemPublicToCrypto(this.publicKey), - ]); - const kek = await keyAgreement(ek.privateKey, clientPublicKey, { - hkdfSalt: await ztdfSalt, - hkdfHash: 'SHA-256', - }); - const iv = generateRandomNumber(12); - const cek = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, kek, dek); - const entityWrappedKey = new Uint8Array(iv.length + cek.byteLength); - entityWrappedKey.set(iv); - entityWrappedKey.set(new Uint8Array(cek), iv.length); - - const policyBinding = await cryptoService.hmac( - hex.encodeArrayBuffer(dek), - base64.encode(policyStr) - ); - - const ephemeralPublicKeyPEM = await cryptoPublicToPem(ek.publicKey); - const kao: KeyAccessObject = { - type: 'ec-wrapped', - url: this.url, - protocol: 'kas', - wrappedKey: base64.encodeArrayBuffer(entityWrappedKey), - encryptedMetadata: base64.encode(encryptedMetadataStr), - policyBinding: { - alg: 'HS256', - hash: base64.encode(policyBinding), - }, - schemaVersion, - ephemeralPublicKey: ephemeralPublicKeyPEM, - }; - if (this.kid) { - kao.kid = this.kid; - } - if (this.sid?.length) { - kao.sid = this.sid; - } - this.keyAccessObject = kao; - return kao; - } -} - -export class Wrapped { - readonly type = 'wrapped'; - keyAccessObject?: KeyAccessObject; - - constructor( - public readonly url: string, - public readonly kid: string | undefined, - public readonly publicKey: string, - public readonly metadata: unknown, - public readonly sid?: string - ) {} - - async write( - policy: Policy, - keyBuffer: Uint8Array, - encryptedMetadataStr: string - ): Promise { - const policyStr = JSON.stringify(policy); - const unwrappedKeyBinary = Binary.fromArrayBuffer(keyBuffer.buffer); - const wrappedKeyBinary = await cryptoService.encryptWithPublicKey( - unwrappedKeyBinary, - this.publicKey - ); - - const policyBinding = await cryptoService.hmac( - hex.encodeArrayBuffer(keyBuffer), - base64.encode(policyStr) - ); - - this.keyAccessObject = { - type: 'wrapped', - url: this.url, - protocol: 'kas', - wrappedKey: base64.encode(wrappedKeyBinary.asString()), - encryptedMetadata: base64.encode(encryptedMetadataStr), - policyBinding: { - alg: 'HS256', - hash: base64.encode(policyBinding), - }, - schemaVersion, - }; - if (this.kid) { - this.keyAccessObject.kid = this.kid; - } - if (this.sid?.length) { - this.keyAccessObject.sid = this.sid; - } - - return this.keyAccessObject; - } -} - -export type KeyAccess = ECWrapped | Wrapped; - /** * A KeyAccess object stores all information about how an object key OR one key split is stored. */ diff --git a/lib/tdf3/src/models/policy.ts b/lib/tdf3/src/models/policy.ts index be4a9d1c..405ed9cf 100644 --- a/lib/tdf3/src/models/policy.ts +++ b/lib/tdf3/src/models/policy.ts @@ -13,7 +13,7 @@ export type Policy = { body?: PolicyBody; }; -export function validatePolicyObject(policyMaybe: unknown): policyMaybe is Policy { +export function isPolicyObject(policyMaybe: unknown): policyMaybe is Policy { if (typeof policyMaybe !== 'object') { throw new ConfigurationError( `The given policy reference must be an object, not: ${policyMaybe}` diff --git a/lib/tdf3/src/splits.ts b/lib/tdf3/src/splits.ts new file mode 100644 index 00000000..b5764806 --- /dev/null +++ b/lib/tdf3/src/splits.ts @@ -0,0 +1,139 @@ +import { keySplit } from './utils/index.js'; +import { base64, hex } from '../../src/encodings/index.js'; +import { Binary } from './binary.js'; +import { type SymmetricCipher } from './ciphers/symmetric-cipher-base.js'; +import { type KeyAccessObject } from './models/key-access.js'; +import { type Policy } from './models/policy.js'; +import { + type CryptoService, + type DecryptResult, + type EncryptResult, +} from './crypto/declarations.js'; +import { ConfigurationError } from '../../src/errors.js'; +import { KeyAccess } from './kao-builders.js'; +import { EncryptionInformation } from './models/encryption-information.js'; + +export type KeyInfo = { + readonly unwrappedKeyBinary: Binary; + readonly unwrappedKeyIvBinary: Binary; +}; + +export class SplitKey { + readonly cryptoService: CryptoService; + keyAccess: KeyAccess[]; + + constructor(public readonly cipher: SymmetricCipher) { + this.cryptoService = cipher.cryptoService; + this.keyAccess = []; + } + + async generateKey(): Promise { + const unwrappedKey = await this.cipher.generateKey(); + const unwrappedKeyBinary = Binary.fromString(hex.decode(unwrappedKey)); + const unwrappedKeyIvBinary = await this.generateIvBinary(); + return { unwrappedKeyBinary, unwrappedKeyIvBinary }; + } + + async encrypt( + contentBinary: Binary, + keyBinary: Binary, + ivBinaryOptional?: Binary + ): Promise { + const ivBinary = ivBinaryOptional || (await this.generateIvBinary()); + return this.cipher.encrypt(contentBinary, keyBinary, ivBinary); + } + + async decrypt(content: Uint8Array, keyBinary: Binary): Promise { + return this.cipher.decrypt(content, keyBinary); + } + + async getKeyAccessObjects(policy: Policy, keyInfo: KeyInfo): Promise { + const splitIds = [...new Set(this.keyAccess.map(({ sid }) => sid))].sort((a = '', b = '') => + a.localeCompare(b) + ); + const unwrappedKeySplitBuffers = await keySplit( + new Uint8Array(keyInfo.unwrappedKeyBinary.asByteArray()), + splitIds.length, + this.cryptoService + ); + const splitsByName = Object.fromEntries( + splitIds.map((sid, index) => [sid, unwrappedKeySplitBuffers[index]]) + ); + + const keyAccessObjects = []; + for (const item of this.keyAccess) { + // use the key split to encrypt metadata for each key access object + const unwrappedKeySplitBuffer = splitsByName[item.sid || '']; + const unwrappedKeySplitBinary = Binary.fromArrayBuffer(unwrappedKeySplitBuffer.buffer); + + const metadata = item.metadata || ''; + let metadataStr; + if (typeof metadata === 'object') { + metadataStr = JSON.stringify(metadata); + } else if (typeof metadata === 'string') { + metadataStr = metadata; + } else { + throw new ConfigurationError("KAO generation failure: metadata isn't a string or object"); + } + + const metadataBinary = Binary.fromArrayBuffer(new TextEncoder().encode(metadataStr)); + + const encryptedMetadataResult = await this.encrypt( + metadataBinary, + unwrappedKeySplitBinary, + keyInfo.unwrappedKeyIvBinary + ); + + const encryptedMetadataOb = { + ciphertext: base64.encode(encryptedMetadataResult.payload.asString()), + iv: base64.encode(keyInfo.unwrappedKeyIvBinary.asString()), + }; + + const encryptedMetadataStr = JSON.stringify(encryptedMetadataOb); + const keyAccessObject = await item.write( + policy, + unwrappedKeySplitBuffer, + encryptedMetadataStr + ); + keyAccessObjects.push(keyAccessObject); + } + + return keyAccessObjects; + } + + async generateIvBinary(): Promise { + const iv = await this.cipher.generateInitializationVector(); + return Binary.fromString(hex.decode(iv)); + } + + async write(policy: Policy, keyInfo: KeyInfo): Promise { + const algorithm = this.cipher?.name; + if (!algorithm) { + // Hard coded as part of the cipher object. This should not be reachable. + throw new ConfigurationError('uninitialized cipher type'); + } + const keyAccessObjects = await this.getKeyAccessObjects(policy, keyInfo); + + // For now we're only concerned with a single (first) key access object + const policyForManifest = base64.encode(JSON.stringify(policy)); + + return { + type: 'split', + keyAccess: keyAccessObjects, + method: { + algorithm, + isStreamable: false, + iv: base64.encode(keyInfo.unwrappedKeyIvBinary.asString()), + }, + integrityInformation: { + rootSignature: { + alg: 'HS256', + sig: '', + }, + segmentHashAlg: 'GMAC', + segments: [], + }, + policy: policyForManifest, + }; + } +} diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index e57804c1..7229dbbb 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -38,23 +38,14 @@ import { type CryptoService, type DecryptResult, } from './crypto/declarations.js'; -import { - ECWrapped, - KeyAccessType, - KeyInfo, - Manifest, - Policy, - SplitKey, - Wrapped, - KeyAccess, - KeyAccessObject, - SplitType, -} from './models/index.js'; +import { ECWrapped, KeyAccess, Wrapped } from './kao-builders.js'; +import { KeyAccessType, Manifest, Policy, KeyAccessObject, SplitType } from './models/index.js'; import { unsigned } from './utils/buffer-crc32.js'; import { ZipReader, ZipWriter, keyMerge, concatUint8, buffToString } from './utils/index.js'; import { CentralDirectory } from './utils/zip-reader.js'; import { ztdfSalt } from './crypto/salt.js'; import { Payload } from './models/payload.js'; +import { KeyInfo, SplitKey } from './splits.js'; // TODO: input validation on manifest JSON const DEFAULT_SEGMENT_SIZE = 1024 * 1024; diff --git a/lib/tdf3/src/utils/index.ts b/lib/tdf3/src/utils/index.ts index 997e8429..4b5c752d 100644 --- a/lib/tdf3/src/utils/index.ts +++ b/lib/tdf3/src/utils/index.ts @@ -1,5 +1,5 @@ import * as WebCryptoService from '../crypto/index.js'; -import { KeyInfo, SplitKey } from '../models/index.js'; +import { KeyInfo, SplitKey } from '../splits.js'; import { AesGcmCipher } from '../ciphers/aes-gcm-cipher.js'; import { ConfigurationError } from '../../../src/errors.js'; diff --git a/lib/tests/mocha/unit/key-access.test.ts b/lib/tests/mocha/unit/key-access.test.ts index 97fd7a00..9b5f69e3 100644 --- a/lib/tests/mocha/unit/key-access.test.ts +++ b/lib/tests/mocha/unit/key-access.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { ECWrapped, Wrapped } from '../../../tdf3/src/models/key-access.js'; +import { ECWrapped, Wrapped } from '../../../tdf3/src/kao-builders.js'; import { Policy } from '../../../tdf3/src/models/policy.js'; import { base64 } from '../../../src/encodings/index.js';