Skip to content

feat(sdk): plain text policy support #694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -309,12 +310,23 @@ async function parseCreateZTDFOptions(argv: Partial<mainArgs>): Promise<CreateZT
return c;
}

async function parseCreateNanoTDFOptions(argv: Partial<mainArgs>): Promise<CreateZTDFOptions> {
async function parseCreateNanoTDFOptions(argv: Partial<mainArgs>): Promise<CreateNanoTDFOptions> {
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)}`);
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
18 changes: 15 additions & 3 deletions lib/src/nanoclients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

/**
Expand Down Expand Up @@ -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.'
Expand Down Expand Up @@ -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
);
}
}
Expand Down Expand Up @@ -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
Expand Down
34 changes: 20 additions & 14 deletions lib/src/nanotdf/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,14 +29,16 @@ 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,
kasInfo: KasPublicKeyInfo,
ephemeralKeyPair: CryptoKeyPair,
iv: Uint8Array,
data: string | ArrayBufferLike,
ecdsaBinding: boolean = DefaultParams.ecdsaBinding
ecdsaBinding: boolean = DefaultParams.ecdsaBinding,
policyType?: PolicyType
): Promise<ArrayBuffer> {
// Generate a symmetric key.
if (!ephemeralKeyPair.privateKey) {
Expand All @@ -54,23 +57,26 @@ 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;

// Calculate the policy binding.
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;
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/nanotdf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
17 changes: 9 additions & 8 deletions lib/src/opentdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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';
Expand Down
Loading