diff --git a/cli/src/cli.ts b/cli/src/cli.ts index caded3d5..88fa606f 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -23,6 +23,7 @@ import { webcrypto } from 'crypto'; import * as assertions from '@opentdf/sdk/assertions'; import { attributeFQNsAsValues } from '@opentdf/sdk/nano'; import { base64 } from '@opentdf/sdk/encodings'; +import { PolicyType } from '@opentdf/sdk'; import { type CryptoKey, importPKCS8, importSPKI } from 'jose'; // for RS256 type AuthToProcess = { @@ -309,12 +310,23 @@ async function parseCreateZTDFOptions(argv: Partial): Promise): Promise { +async function parseCreateNanoTDFOptions(argv: Partial): Promise { const c: CreateNanoTDFOptions = await parseCreateOptions(argv); const ecdsaBinding = argv.policyBinding?.toLowerCase() == 'ecdsa'; if (ecdsaBinding) { c.bindingType = 'ecdsa'; } + + if (argv.policyType) { + switch (argv.policyType) { + case 'EmbeddedEncrypted': + c.policyType = PolicyType.EmbeddedEncrypted; + break; + case 'EmbeddedText': + c.policyType = PolicyType.EmbeddedText; + break; + } + } // NOTE autoconfigure is not yet supported in nanotdf delete c.autoconfigure; log('DEBUG', `CreateNanoTDFOptions: ${JSON.stringify(c)}`); @@ -521,6 +533,13 @@ export const handleArgs = (args: string[]) => { description: 'TDF spec version for file creation', default: tdfSpecVersion, }, + policyType: { + group: 'Encrypt Options:', + desc: 'Policy type for NanoTDF: EmbeddedEncrypted or EmbeddedText', + type: 'string', + choices: ['EmbeddedEncrypted', 'EmbeddedText'], + default: 'EmbeddedEncrypted', + }, }) .options({ diff --git a/lib/src/index.ts b/lib/src/index.ts index 923bc8e7..2a4ef1ba 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -6,3 +6,4 @@ export { PlatformClient, type PlatformClientOptions, type PlatformServices } fro export * from './opentdf.js'; export * from './seekable.js'; export * from '../tdf3/src/models/index.js'; +export { default as PolicyType } from './nanotdf/enum/PolicyTypeEnum.js'; diff --git a/lib/src/nanoclients.ts b/lib/src/nanoclients.ts index 845ff896..f90413f2 100644 --- a/lib/src/nanoclients.ts +++ b/lib/src/nanoclients.ts @@ -14,15 +14,18 @@ import { fetchECKasPubKey } from './access.js'; import { type ClientConfig } from './nanotdf/Client.js'; import { ConfigurationError } from './errors.js'; import { type AttributeObject } from '../tdf3/src/models/attribute.js'; +import PolicyType from './nanotdf/enum/PolicyTypeEnum.js'; // Define the EncryptOptions type export type EncryptOptions = { ecdsaBinding: boolean; + policyType?: PolicyType; }; // Define default options const defaultOptions: EncryptOptions = { ecdsaBinding: false, + policyType: PolicyType.EmbeddedEncrypted, }; /** @@ -100,6 +103,14 @@ export class NanoTDFClient extends Client { const ephemeralKeyPair = await this.ephemeralKeyPair; const initializationVector = this.iv; + const mergedOptions: EncryptOptions = { ...defaultOptions, ...options }; + if ( + mergedOptions.policyType !== PolicyType.EmbeddedEncrypted && + mergedOptions.policyType !== PolicyType.EmbeddedText + ) { + throw new ConfigurationError('Invalid policy type'); + } + if (typeof initializationVector !== 'number') { throw new ConfigurationError( 'NanoTDF clients are single use. Please generate a new client and keypair.' @@ -146,14 +157,14 @@ export class NanoTDFClient extends Client { payloadIV[10] = lengthAsUint24[1]; payloadIV[11] = lengthAsUint24[0]; - const mergedOptions: EncryptOptions = { ...defaultOptions, ...options }; return encrypt( policyObjectAsStr, this.kasPubKey, ephemeralKeyPair, payloadIV, data, - mergedOptions.ecdsaBinding + mergedOptions.ecdsaBinding, + mergedOptions.policyType ); } } @@ -283,7 +294,8 @@ export class NanoTDFDatasetClient extends Client { ephemeralKeyPair, ivVector, data, - this.ecdsaBinding + this.ecdsaBinding, + mergedOptions.policyType ); // Cache the header and increment the key iteration diff --git a/lib/src/nanotdf/encrypt.ts b/lib/src/nanotdf/encrypt.ts index be0c6860..484f6c6e 100644 --- a/lib/src/nanotdf/encrypt.ts +++ b/lib/src/nanotdf/encrypt.ts @@ -18,6 +18,7 @@ import { import { KasPublicKeyInfo } from '../access.js'; import { computeECDSASig, extractRSValuesFromSignature } from '../nanotdf-crypto/ecdsaSignature.js'; import { ConfigurationError } from '../errors.js'; +import PolicyType from './enum/PolicyTypeEnum.js'; /** * Encrypt the plain data into nanotdf buffer @@ -28,6 +29,7 @@ import { ConfigurationError } from '../errors.js'; * @param iv * @param data The data to be encrypted * @param ecdsaBinding Flag to enable ECDSA binding + * @param policyType Policy type to use for the nanotdf */ export default async function encrypt( policy: string, @@ -35,7 +37,8 @@ export default async function encrypt( ephemeralKeyPair: CryptoKeyPair, iv: Uint8Array, data: string | ArrayBufferLike, - ecdsaBinding: boolean = DefaultParams.ecdsaBinding + ecdsaBinding: boolean = DefaultParams.ecdsaBinding, + policyType?: PolicyType ): Promise { // Generate a symmetric key. if (!ephemeralKeyPair.privateKey) { @@ -54,15 +57,18 @@ export default async function encrypt( // Auth tag length for policy and payload const authTagLengthInBytes = authTagLengthForCipher(DefaultParams.symmetricCipher) / 8; - // Encrypt the policy - const policyIV = new Uint8Array(iv.length).fill(0); - const policyAsBuffer = new TextEncoder().encode(policy); - const encryptedPolicy = await cryptoEncrypt( - symmetricKey, - policyAsBuffer, - policyIV, - authTagLengthInBytes * 8 - ); + let policyContent: Uint8Array; + if (policyType === PolicyType.EmbeddedText) { + // Store policy as plain text + policyContent = new TextEncoder().encode(policy); + } else { + // Encrypt the policy + const policyIV = new Uint8Array(iv.length).fill(0); + const policyAsBuffer = new TextEncoder().encode(policy); + policyContent = new Uint8Array( + await cryptoEncrypt(symmetricKey, policyAsBuffer, policyIV, authTagLengthInBytes * 8) + ); + } let policyBinding: Uint8Array; @@ -70,7 +76,7 @@ export default async function encrypt( if (ecdsaBinding) { const curveName = await getCurveNameFromPrivateKey(ephemeralKeyPair.privateKey); const ecdsaPrivateKey = await convertECDHToECDSA(ephemeralKeyPair.privateKey, curveName); - const ecdsaSignature = await computeECDSASig(ecdsaPrivateKey, new Uint8Array(encryptedPolicy)); + const ecdsaSignature = await computeECDSASig(ecdsaPrivateKey, policyContent); const { r, s } = extractRSValuesFromSignature(new Uint8Array(ecdsaSignature)); const rLength = r.length; @@ -84,15 +90,15 @@ export default async function encrypt( policyBinding[1 + rLength] = sLength; policyBinding.set(s, 1 + rLength + 1); } else { - const signature = await digest('SHA-256', new Uint8Array(encryptedPolicy)); + const signature = await digest('SHA-256', policyContent); policyBinding = new Uint8Array(signature.slice(-GMAC_BINDING_LEN)); } // Create embedded policy const embeddedPolicy = new EmbeddedPolicy( - DefaultParams.policyType, + policyType ?? PolicyType.EmbeddedEncrypted, policyBinding, - new Uint8Array(encryptedPolicy) + policyContent ); if (!ephemeralKeyPair.publicKey) { diff --git a/lib/src/nanotdf/index.ts b/lib/src/nanotdf/index.ts index 5b009dec..16e63f7e 100644 --- a/lib/src/nanotdf/index.ts +++ b/lib/src/nanotdf/index.ts @@ -8,3 +8,4 @@ export { default as encrypt } from './encrypt.js'; export { default as encryptDataset } from './encrypt-dataset.js'; export { default as getHkdfSalt } from './helpers/getHkdfSalt.js'; export { default as DefaultParams } from './models/DefaultParams.js'; +export { default as PolicyType } from './enum/PolicyTypeEnum.js'; diff --git a/lib/src/opentdf.ts b/lib/src/opentdf.ts index 69b51be2..18891dbe 100644 --- a/lib/src/opentdf.ts +++ b/lib/src/opentdf.ts @@ -92,6 +92,9 @@ export type CreateNanoTDFOptions = CreateOptions & { * to generate a signature for each element. When absent, the nanotdf is unsigned. */ signingKeyID?: string; + + /** The type of policy to embed in the NanoTDF. */ + policyType?: PolicyType; }; /** Options for creating a NanoTDF collection. */ @@ -759,14 +762,12 @@ class Collection { if (opts.ecdsaBindingKeyID) { throw new ConfigurationError('custom binding key not implemented'); } - switch (opts.bindingType) { - case 'ecdsa': - this.encryptOptions = { ecdsaBinding: true }; - break; - case 'gmac': - this.encryptOptions = { ecdsaBinding: false }; - break; - } + + // Initialize encryptOptions with policyType if provided + this.encryptOptions = { + ecdsaBinding: opts.bindingType === 'ecdsa', + policyType: opts.policyType, + }; const kasEndpoint = opts.defaultKASEndpoint || opts.platformUrl || 'https://disallow.all.invalid';