Skip to content
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

chore: adds multi-file encrypt and decrypt to web-app #279

Closed
wants to merge 8 commits into from
Closed
Changes from 1 commit
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
Next Next commit
feat!: lets nanoTDF client take options instead
- Adds `tsc` build of web-app to makefile
- fixes existing typescript issues in web app, and some cleanup
  • Loading branch information
dmihalcik-virtru committed May 9, 2024
commit 8fc299b2d9df36b49a7ccb97f08b06e9ff47d42c
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ remote-store/opentdf-remote-store-$(version).tgz: lib/opentdf-client-$(version).
(cd remote-store && npm ci ../lib/opentdf-client-$(version).tgz && npm pack)

web-app/opentdf-web-app-$(version).tgz: lib/opentdf-client-$(version).tgz $(shell find web-app -not -path '*/dist*' -and -not -path '*/coverage*' -and -not -path '*/node_modules*')
(cd web-app && npm ci ../lib/opentdf-client-$(version).tgz && npm pack)
(cd web-app && npm ci ../lib/opentdf-client-$(version).tgz && npm pack && npm run build)

lib/opentdf-client-$(version).tgz: $(shell find lib -not -path '*/dist*' -and -not -path '*/coverage*' -and -not -path '*/node_modules*')
(cd lib && npm ci --including=dev && npm pack)
2 changes: 1 addition & 1 deletion lib/README.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ TDF3 with JSON envelopes.
oidcOrigin: keycloakUrl,
}
const authProvider = await AuthProviders.refreshAuthProvider(oidcCredentials);
const client = new NanoTDFClient(authProvider, access);
const client = new NanoTDFClient({authProvider, kasEndpoint});
const cipherText = await client.encrypt(plainText);
const clearText = await client.decrypt(cipherText);
```
48 changes: 26 additions & 22 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import {
} from './nanotdf/index.js';
import { keyAgreement, extractPublicFromCertToCrypto } from './nanotdf-crypto/index.js';
import { TypedArray, createAttribute, Policy } from './tdf/index.js';
import { type AuthProvider } from './auth/auth.js';
import { ClientConfig } from './nanotdf/Client.js';

async function fetchKasPubKey(kasUrl: string): Promise<string> {
const kasPubKeyResponse = await fetch(`${kasUrl}/kas_public_key?algorithm=ec:secp256r1`);
@@ -33,13 +33,14 @@ async function fetchKasPubKey(kasUrl: string): Promise<string> {
* const KAS_URL = 'http://localhost:65432/api/kas/';
*
* const ciphertext = '...';
* const client = new NanoTDFClient(
* await clientSecretAuthProvider({
* const client = new NanoTDFClient({
* authProvider: await clientSecretAuthProvider({
* clientId: 'tdf-client',
* clientSecret: '123-456',
* oidcOrigin: OIDC_ENDPOINT,
* }),
* KAS_URL
* kasEndpoint: KAS_URL
* }
* );
* client.decrypt(ciphertext)
* .then(plaintext => {
@@ -120,9 +121,9 @@ export class NanoTDFClient extends Client {
*/
async encrypt(data: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
// For encrypt always generate the client ephemeralKeyPair
const ephemeralKeyPair = await this.generateEphemeralKeyPair();

const ephemeralKeyPair = await this.ephemeralKeyPair;
const initializationVector = this.iv;

if (typeof initializationVector !== 'number') {
throw new Error('NanoTDF clients are single use. Please generate a new client and keypair.');
}
@@ -174,6 +175,10 @@ export class NanoTDFClient extends Client {
}
}

export type DatasetConfig = ClientConfig & {
maxKeyIterations?: number;
};

/**
* NanoTDF Dataset SDK Client
*
@@ -186,15 +191,15 @@ export class NanoTDFClient extends Client {
* const KAS_URL = 'http://localhost:65432/api/kas/';
*
* const ciphertext = '...';
* const client = new NanoTDFDatasetClient.default(
* await clientSecretAuthProvider({
* const client = new NanoTDFDatasetClient({
* authProvider: await clientSecretAuthProvider({
* clientId: 'tdf-client',
* clientSecret: '123-456',
* exchange: 'client',
* oidcOrigin: OIDC_ENDPOINT,
* }),
* KAS_URL
* );
* kasEndpoint: KAS_URL,
* });
* const plaintext = client.decrypt(ciphertext);
* console.log('Plaintext', plaintext);
* ```
@@ -223,19 +228,18 @@ export class NanoTDFDatasetClient extends Client {
* @param ephemeralKeyPair (optional) ephemeral key pair to use
* @param maxKeyIterations Max iteration to performe without a key rotation
*/
constructor(
authProvider: AuthProvider,
kasUrl: string,
maxKeyIterations: number = NanoTDFDatasetClient.NTDF_MAX_KEY_ITERATIONS,
ephemeralKeyPair?: Required<Readonly<CryptoKeyPair>>
) {
if (maxKeyIterations > NanoTDFDatasetClient.NTDF_MAX_KEY_ITERATIONS) {
throw new Error('Key iteration exceeds max iterations(8388606)');
constructor(opts: DatasetConfig) {
if (
opts.maxKeyIterations &&
opts.maxKeyIterations > NanoTDFDatasetClient.NTDF_MAX_KEY_ITERATIONS
) {
throw new Error(
`Key iteration exceeds max iterations(${NanoTDFDatasetClient.NTDF_MAX_KEY_ITERATIONS})`
);
}
super(opts);

super(authProvider, kasUrl, ephemeralKeyPair);

this.maxKeyIteration = maxKeyIterations;
this.maxKeyIteration = opts.maxKeyIterations || NanoTDFDatasetClient.NTDF_MAX_KEY_ITERATIONS;
this.keyIterationCount = 0;
}

@@ -250,7 +254,7 @@ export class NanoTDFDatasetClient extends Client {
// Intial encrypt
if (this.keyIterationCount == 0) {
// For encrypt always generate the client ephemeralKeyPair
const ephemeralKeyPair = await this.generateEphemeralKeyPair();
const ephemeralKeyPair = await this.ephemeralKeyPair;

if (!this.kasPubKey) {
this.kasPubKey = await fetchKasPubKey(this.kasUrl);
154 changes: 86 additions & 68 deletions lib/src/nanotdf/Client.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,59 @@ import { cryptoPublicToPem, safeUrlCheck, validateSecureUrl } from '../utils.js'

const { KeyUsageType, AlgorithmName, NamedCurve } = cryptoEnums;

export interface ClientConfig {
authProvider: AuthProvider;
dpopEnabled?: boolean;
dpopKeys?: Promise<CryptoKeyPair>;
ephemeralKeyPair?: Promise<CryptoKeyPair>;
kasEndpoint: string;
}

function toJWSAlg(c: CryptoKey): string {
const { algorithm } = c;
switch (algorithm.name) {
case 'RSASSA-PKCS1-v1_5':
case 'RSA-PSS':
case 'RSA-OAEP': {
const r = algorithm as RsaHashedKeyGenParams;
switch (r.modulusLength) {
case 2048:
return 'RS256';
case 3072:
return 'RS384';
case 3072:
return 'RS512';
}
}
case 'ECDSA':
case 'ECDH': {
return 'ES256';
}
}
throw new Error(`Unsupported key algorithm ${JSON.stringify(algorithm)}`);
}

async function generateEphemeralKeyPair(): Promise<CryptoKeyPair> {
const { publicKey, privateKey } = await generateKeyPair();
if (!privateKey || !publicKey) {
throw Error('Key pair generation failed');
}
return { publicKey, privateKey };
}

async function generateSignerKeyPair(): Promise<CryptoKeyPair> {
const { publicKey, privateKey } = await generateKeyPair({
type: AlgorithmName.ECDSA,
curve: NamedCurve.P256,
keyUsages: [KeyUsageType.Sign, KeyUsageType.Verify],
isExtractable: true,
});
if (!privateKey || !publicKey) {
throw Error('Signer key pair generation failed');
}
return { publicKey, privateKey };
}

/**
* A Client encapsulates sessions interacting with TDF3 and nanoTDF backends, KAS and any
* plugin-based sessions like identity and further attribute control. Most importantly, it is responsible
@@ -63,8 +116,8 @@ export default class Client {
readonly dpopEnabled: boolean;
dissems: string[] = [];
dataAttributes: string[] = [];
protected ephemeralKeyPair?: Required<Readonly<CryptoKeyPair>>;
protected requestSignerKeyPair?: Required<Readonly<CryptoKeyPair>>;
protected ephemeralKeyPair: Promise<CryptoKeyPair>;
protected requestSignerKeyPair: Promise<CryptoKeyPair>;
protected iv?: number;

/**
@@ -74,59 +127,32 @@ export default class Client {
* cannot be changed. If a new ephemeral key is desired it a new client should be initialized.
* There is no performance impact for creating a new client IFF the ephemeral key pair is provided.
*/
constructor(
authProvider: AuthProvider,
kasUrl: string,
ephemeralKeyPair?: Required<Readonly<CryptoKeyPair>>,
dpopEnabled = false
) {
constructor({
authProvider,
ephemeralKeyPair,
kasEndpoint,
dpopEnabled,
dpopKeys,
}: ClientConfig) {
this.authProvider = authProvider;
// TODO Disallow http KAS. For now just log as error
validateSecureUrl(kasUrl);
this.kasUrl = kasUrl;
this.allowedKases = [kasUrl];
validateSecureUrl(kasEndpoint);
this.kasUrl = kasEndpoint;
this.allowedKases = [kasEndpoint];
this.kasPubKey = '';
this.dpopEnabled = dpopEnabled;
this.dpopEnabled = !!dpopEnabled;
if (dpopKeys) {
this.requestSignerKeyPair = dpopKeys;
} else {
this.requestSignerKeyPair = generateSignerKeyPair();
}

if (ephemeralKeyPair) {
this.ephemeralKeyPair = ephemeralKeyPair;
this.iv = 1;
} else {
this.ephemeralKeyPair = generateEphemeralKeyPair();
}
}

/**
* Get ephemeral key pair
*
* Returns the ephemeral key pair to be used in other clients or undefined if not set or generated
*
* @security allow returning ephemeral key pair has unknown security risks.
*/
getEphemeralKeyPair(): CryptoKeyPair | undefined {
return this.ephemeralKeyPair;
}

async generateEphemeralKeyPair(): Promise<Required<Readonly<CryptoKeyPair>>> {
const { publicKey, privateKey } = await generateKeyPair();
if (!privateKey || !publicKey) {
throw Error('Key pair generation failed');
}
this.ephemeralKeyPair = { publicKey, privateKey };
this.iv = 1;
return { publicKey, privateKey };
}

async generateSignerKeyPair(): Promise<Required<Readonly<CryptoKeyPair>>> {
const { publicKey, privateKey } = await generateKeyPair({
type: AlgorithmName.ECDSA,
curve: NamedCurve.P256,
keyUsages: [KeyUsageType.Sign, KeyUsageType.Verify],
isExtractable: true,
});
if (!privateKey || !publicKey) {
throw Error('Signer key pair generation failed');
}
this.requestSignerKeyPair = { publicKey, privateKey };
return { publicKey, privateKey };
}

/**
@@ -150,18 +176,7 @@ export default class Client {
* either be set on the first call or passed in the constructor.
*/
async fetchOIDCToken(): Promise<void> {
// Generate the ephemeral key pair if not set
const promises: Promise<Required<Readonly<CryptoKeyPair>>>[] = [];
if (!this.ephemeralKeyPair) {
promises.push(this.generateEphemeralKeyPair());
}

if (!this.requestSignerKeyPair) {
promises.push(this.generateSignerKeyPair());
}
await Promise.all(promises);

const signer = this.requestSignerKeyPair;
const signer = await this.requestSignerKeyPair;
if (!signer) {
throw new Error('Unexpected state');
}
@@ -190,13 +205,15 @@ export default class Client {

// Ensure the ephemeral key pair has been set or generated (see createOidcServiceProvider)
await this.fetchOIDCToken();
const ephemeralKeyPair = await this.ephemeralKeyPair;
const requestSignerKeyPair = await this.requestSignerKeyPair;

// Ensure the ephemeral key pair has been set or generated (see fetchEntityObject)
if (!this.ephemeralKeyPair?.privateKey) {
if (!ephemeralKeyPair?.privateKey) {
throw new Error('Ephemeral key has not been set or generated');
}

if (!this.requestSignerKeyPair?.privateKey) {
if (!requestSignerKeyPair?.privateKey) {
throw new Error('Signer key has not been set or generated');
}

@@ -210,13 +227,13 @@ export default class Client {
protocol: Client.KAS_PROTOCOL,
header: base64.encodeArrayBuffer(nanoTdfHeader),
},
clientPublicKey: await cryptoPublicToPem(this.ephemeralKeyPair.publicKey),
clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
});

const jwtPayload = { requestBody: requestBodyStr };
const requestBody = {
signedRequestToken: await reqSignature(jwtPayload, this.requestSignerKeyPair.privateKey, {
alg: AlgorithmName.ES256,
signedRequestToken: await reqSignature(jwtPayload, requestSignerKeyPair.privateKey, {
alg: toJWSAlg(requestSignerKeyPair.publicKey),
}),
};

@@ -239,10 +256,10 @@ export default class Client {
const iv = entityWrappedKey.subarray(0, ivLength);
const encryptedSharedKey = entityWrappedKey.subarray(ivLength);

let publicKey;
let kasPublicKey;
try {
// Get session public key as crypto key
publicKey = await pemPublicToCrypto(wrappedKey.sessionPublicKey);
kasPublicKey = await pemPublicToCrypto(wrappedKey.sessionPublicKey);
} catch (cause) {
throw new Error(
`PEM Public Key to crypto public key failed. Is PEM formatted correctly?\n Caused by: ${cause.message}`,
@@ -257,12 +274,13 @@ export default class Client {
} catch (e) {
throw new Error(`Salting hkdf failed\n Caused by: ${e.message}`);
}
const { privateKey } = await this.ephemeralKeyPair;

// Get the unwrapping key
const unwrappingKey = await keyAgreement(
// Ephemeral private key
this.ephemeralKeyPair.privateKey,
publicKey,
privateKey,
kasPublicKey,
hkdfSalt
);

2 changes: 2 additions & 0 deletions lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { IllegalArgumentError } from '../../../src/errors.js';
import { PemKeyPair } from '../crypto/declarations.js';
import { EntityObject } from '../../../src/tdf/EntityObject.js';
import { DecoratedReadableStream } from './DecoratedReadableStream.js';
import { type Chunker } from '../utils/chunkers.js';

export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;
export type Scope = {
@@ -470,6 +471,7 @@ export type DecryptStreamMiddleware = (

export type DecryptSource =
| { type: 'buffer'; location: Uint8Array }
| { type: 'chunker'; location: Chunker }
| { type: 'remote'; location: string }
| { type: 'stream'; location: ReadableStream<Uint8Array> }
| { type: 'file-browser'; location: Blob };
Loading
Loading