Skip to content

Commit

Permalink
feat!(sdk): New client object api
Browse files Browse the repository at this point in the history
- Creates new type `OpenTDF`, which adds 'create' and 'read' methods for TDF objects
- Sequesters old APIs into `@opentdf/sdk/singlecontainer` entry point. If the old API is desired, use this import, not `@opentdf/sdk`.
  • Loading branch information
dmihalcik-virtru committed Dec 13, 2024
1 parent a67011c commit bcc7a27
Show file tree
Hide file tree
Showing 22 changed files with 785 additions and 384 deletions.
2 changes: 1 addition & 1 deletion cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

216 changes: 76 additions & 140 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import './polyfills.js';
import { createWriteStream, openAsBlob } from 'node:fs';
import { readFile, stat, writeFile } from 'node:fs/promises';
import { stat, writeFile } from 'node:fs/promises';
import { Writable } from 'node:stream';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import {
type AuthProvider,
type EncryptParams,
type CreateOptions,
type CreateNanoTDFOptions,
type CreateZTDFOptions,
type HttpRequest,
type ReadOptions,
type Source,
AuthProviders,
NanoTDFClient,
NanoTDFDatasetClient,
TDF3Client,
version,
EncryptParamsBuilder,
DecryptParams,
DecryptParamsBuilder,
OpenTDF,
} from '@opentdf/sdk';
import { CLIError, Level, log } from './logger.js';
import { webcrypto } from 'crypto';
Expand Down Expand Up @@ -108,30 +107,17 @@ const rstrip = (str: string, suffix = ' '): string => {
return str;
};

type AnyNanoClient = NanoTDFClient | NanoTDFDatasetClient;

function addParams(client: AnyNanoClient, argv: Partial<mainArgs>) {
if (argv.attributes?.length) {
client.dataAttributes = argv.attributes.split(',');
}
if (argv.usersWithAccess?.length) {
client.dissems = argv.usersWithAccess.split(',');
}
log('SILLY', `Built encrypt params dissems: ${client.dissems}, attrs: ${client.dataAttributes}`);
}

async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptParams> {
const c = new DecryptParamsBuilder();
async function parseReadOptions(argv: Partial<mainArgs>): Promise<ReadOptions> {
const r: ReadOptions = { source: await fileAsSource(argv.file as string) };
if (argv.noVerifyAssertions) {
c.withNoVerifyAssertions(true);
r.noVerify = true;
}
if (argv.concurrencyLimit) {
c.withConcurrencyLimit(argv.concurrencyLimit);
if (argv.concurrencyLimit !== undefined) {
r.concurrencyLimit = argv.concurrencyLimit;
} else {
c.withConcurrencyLimit(100);
r.concurrencyLimit = 100;
}
c.setFileSource(await openAsBlob(argv.file as string));
return c.build();
return r;
}

function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
Expand All @@ -149,32 +135,42 @@ function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
return a;
}

async function tdf3EncryptParamsFor(argv: Partial<mainArgs>): Promise<EncryptParams> {
const c = new EncryptParamsBuilder();
if (argv.assertions?.length) {
c.withAssertions(parseAssertionConfig(argv.assertions));
}
async function parseCreateOptions(argv: Partial<mainArgs>): Promise<CreateOptions> {
const c: CreateOptions = {
source: { type: 'file-browser', location: await openAsBlob(argv.file as string) },
};
if (argv.attributes?.length) {
c.setAttributes(argv.attributes.split(','));
c.attributes = argv.attributes.split(',');
}
if (argv.usersWithAccess?.length) {
c.setUsersWithAccess(argv.usersWithAccess.split(','));
c.autoconfigure = !!argv.autoconfigure;
return c;
}

async function parseCreateZTDFOptions(argv: Partial<mainArgs>): Promise<CreateZTDFOptions> {
const c: CreateZTDFOptions = await parseCreateOptions(argv);
if (argv.assertions?.length) {
c.assertionConfigs = parseAssertionConfig(argv.assertions);
}
if (argv.mimeType?.length) {
c.setMimeType(argv.mimeType);
if (argv.mimeType && /^[a-z]+\/[a-z0-9-+.]+$/.test(argv.mimeType)) {
c.mimeType = argv.mimeType as `${string}/${string}`;
} else {
throw new CLIError('CRITICAL', 'Invalid mimeType format');
}
}
if (argv.autoconfigure) {
c.withAutoconfigure();
return c;
}

async function parseCreateNanoTDFOptions(argv: Partial<mainArgs>): Promise<CreateZTDFOptions> {
const c: CreateNanoTDFOptions = await parseCreateOptions(argv);
const ecdsaBinding = argv.policyBinding?.toLowerCase() == 'ecdsa';
if (ecdsaBinding) {
c.bindingType = 'ecdsa';
}
// use offline mode, we do not have upsert for v2
c.setOffline();
// FIXME TODO must call file.close() after we are done
const buffer = await processDataIn(argv.file as string);
c.setBufferSource(buffer);
return c.build();
return c;
}

async function processDataIn(file: string) {
async function fileAsSource(file: string): Promise<Source> {
if (!file) {
throw new CLIError('CRITICAL', 'Must specify file or pipe');
}
Expand All @@ -187,7 +183,7 @@ async function processDataIn(file: string) {
throw new CLIError('CRITICAL', `File is not accessable [${file}]`);
}
log('DEBUG', `Using input from file [${file}]`);
return readFile(file);
return { type: 'file-browser', location: await openAsBlob(file) };
}

export const handleArgs = (args: string[]) => {
Expand Down Expand Up @@ -343,14 +339,6 @@ export const handleArgs = (args: string[]) => {
type: 'string',
description: 'Owner email address',
},
usersWithAccess: {
alias: 'users-with-access',
group: 'Encrypt Options:',
desc: 'Add users to the policy',
type: 'string',
default: '',
validate: (users: string) => users.split(','),
},
})

// COMMANDS
Expand Down Expand Up @@ -422,56 +410,25 @@ export const handleArgs = (args: string[]) => {
const ignoreAllowList = !!argv.ignoreAllowList;
const authProvider = await processAuth(argv);
log('DEBUG', `Initialized auth provider ${JSON.stringify(authProvider)}`);
const client = new OpenTDF({
authProvider,
defaultCreateOptions: {
defaultKASEndpoint: argv.kasEndpoint,
},
defaultReadOptions: {
allowedKASEndpoints: allowedKases,
ignoreAllowlist: ignoreAllowList,
noVerify: !!argv.noVerifyAssertions,
},
disableDPoP: !argv.dpop,
policyEndpoint: guessPolicyUrl(argv),
});
log('SILLY', `Initialized client ${JSON.stringify(client)}`);

const kasEndpoint = argv.kasEndpoint;
if (argv.containerType === 'tdf3' || argv.containerType == 'ztdf') {
log('DEBUG', `TDF3 Client`);
const client = new TDF3Client({
allowedKases,
ignoreAllowList,
authProvider,
kasEndpoint,
dpopEnabled: argv.dpop,
});
log('SILLY', `Initialized client ${JSON.stringify(client)}`);
log('DEBUG', `About to decrypt [${argv.file}]`);
const ct = await client.decrypt(await tdf3DecryptParamsFor(argv));
if (argv.output) {
const destination = createWriteStream(argv.output);
await ct.stream.pipeTo(Writable.toWeb(destination));
} else {
console.log(await ct.toString());
}
} else {
const dpopEnabled = !!argv.dpop;
const client =
argv.containerType === 'nano'
? new NanoTDFClient({
allowedKases,
ignoreAllowList,
authProvider,
kasEndpoint,
dpopEnabled,
})
: new NanoTDFDatasetClient({
allowedKases,
ignoreAllowList,
authProvider,
kasEndpoint,
dpopEnabled,
});
const buffer = await processDataIn(argv.file as string);

log('DEBUG', 'Decrypt data.');
const plaintext = await client.decrypt(buffer);

log('DEBUG', 'Handle output.');
if (argv.output) {
await writeFile(argv.output, new Uint8Array(plaintext));
} else {
console.log(new TextDecoder().decode(plaintext));
}
}
log('DEBUG', `About to TDF3 decrypt [${argv.file}]`);
const ct = await client.read(await parseReadOptions(argv));
const destination = createWriteStream(argv.output as string);
await ct.pipeTo(Writable.toWeb(destination));
const lastRequest = authProvider.requestLog[authProvider.requestLog.length - 1];
let accessToken = null;
let dpopToken = null;
Expand Down Expand Up @@ -510,50 +467,29 @@ export const handleArgs = (args: string[]) => {
log('DEBUG', 'Running encrypt command');
const authProvider = await processAuth(argv);
log('DEBUG', `Initialized auth provider ${JSON.stringify(authProvider)}`);
const kasEndpoint = argv.kasEndpoint;
const ignoreAllowList = !!argv.ignoreAllowList;
const allowedKases = argv.allowList?.split(',');

const client = new OpenTDF({
authProvider,
defaultCreateOptions: {
defaultKASEndpoint: argv.kasEndpoint,
},
disableDPoP: !argv.dpop,
policyEndpoint: guessPolicyUrl(argv),
});
log('SILLY', `Initialized client ${JSON.stringify(client)}`);

if ('tdf3' === argv.containerType || 'ztdf' === argv.containerType) {
log('DEBUG', `TDF3 Client`);
const policyEndpoint: string = guessPolicyUrl(argv);
const client = new TDF3Client({
allowedKases,
ignoreAllowList,
authProvider,
kasEndpoint,
policyEndpoint,
dpopEnabled: argv.dpop,
});
log('SILLY', `Initialized client ${JSON.stringify(client)}`);
const ct = await client.encrypt(await tdf3EncryptParamsFor(argv));
log('DEBUG', `TDF3 Create`);
const ct = await client.createZTDF(await parseCreateZTDFOptions(argv));
if (!ct) {
throw new CLIError('CRITICAL', 'Encrypt configuration error: No output?');
}
if (argv.output) {
const destination = createWriteStream(argv.output);
await ct.stream.pipeTo(Writable.toWeb(destination));
} else {
console.log(await ct.toString());
}
const destination = createWriteStream(argv.output as string);
await ct.pipeTo(Writable.toWeb(destination));
} else {
const dpopEnabled = !!argv.dpop;
const ecdsaBinding = argv.policyBinding.toLowerCase() == 'ecdsa';
const client =
argv.containerType === 'nano'
? new NanoTDFClient({ allowedKases, authProvider, dpopEnabled, kasEndpoint })
: new NanoTDFDatasetClient({
allowedKases,
authProvider,
dpopEnabled,
kasEndpoint,
});
log('SILLY', `Initialized client ${JSON.stringify(client)}`);

addParams(client, argv);

const buffer = await processDataIn(argv.file as string);
const cyphertext = await client.encrypt(buffer, { ecdsaBinding });
const cyphertext = await client.createNanoTDF(await parseCreateNanoTDFOptions(argv));

log('DEBUG', `Handle cyphertext output ${JSON.stringify(cyphertext)}`);
if (argv.output) {
Expand Down
6 changes: 0 additions & 6 deletions lib/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"main": "./dist/cjs/tdf3/index.js",
"exports": {
".": {
"types": "./dist/types/src/index.d.ts",
"require": "./dist/cjs/src/index.js",
"import": "./dist/web/src/index.js"
},
"./singlecontainer": {
"types": "./dist/types/tdf3/index.d.ts",
"require": "./dist/cjs/tdf3/index.js",
"import": "./dist/web/tdf3/index.js"
Expand All @@ -44,9 +49,9 @@
}
},
"./nano": {
"types": "./dist/types/src/index.d.ts",
"require": "./dist/cjs/src/index.js",
"import": "./dist/web/src/index.js"
"types": "./dist/types/src/nanoindex.d.ts",
"require": "./dist/cjs/src/nanoindex.js",
"import": "./dist/web/src/nanoindex.js"
}
},
"scripts": {
Expand Down Expand Up @@ -85,7 +90,6 @@
"@types/node": "^20.4.5",
"@types/send": "^0.17.1",
"@types/sinon": "~10.0.15",
"@types/streamsaver": "^2.0.1",
"@types/uuid": "~9.0.2",
"@types/wicg-file-system-access": "^2020.9.6",
"@typescript-eslint/eslint-plugin": "^6.2.1",
Expand Down
4 changes: 3 additions & 1 deletion lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { type AuthProvider, type HttpMethod, HttpRequest, withHeaders } from './auth/auth.js';
export * as AuthProviders from './auth/providers.js';
export { attributeFQNsAsValues } from './policy/api.js';
export * from './nanoclients.js';
export { version, clientType } from './version.js';
export * from './opentdf.js';
export * from './seekable.js';
1 change: 1 addition & 0 deletions lib/src/nanoclients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class NanoTDFClient extends Client {
async decryptLegacyTDF(ciphertext: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
// Parse ciphertext
const nanotdf = NanoTDF.from(ciphertext, undefined, true);

const legacyVersion = '0.0.0';
// Rewrap key on every request
const key = await this.rewrapKey(
Expand Down
4 changes: 4 additions & 0 deletions lib/src/nanoindex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * as AuthProviders from './auth/providers.js';
export { attributeFQNsAsValues } from './policy/api.js';
export * from './nanoclients.js';
export { version, clientType } from './version.js';
Loading

0 comments on commit bcc7a27

Please sign in to comment.