diff --git a/lib/src/index.ts b/lib/src/index.ts index a5660956..ff6b0b9f 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -16,19 +16,19 @@ import { TypedArray, createAttribute, Policy } from './tdf/index'; * * @example * ``` - * import NanoTDFClient from '@opentdf/client'; + * import { clientSecretAuthProvider, NanoTDFClient } from '@opentdf/client'; * - * const OIDC_ENDPOINT = 'http://localhost:65432/keycloak/'; - * const KAS_URL = 'http://localhost:65432/kas'; + * const OIDC_ENDPOINT = 'http://localhost:65432/auth/'; + * const KAS_URL = 'http://localhost:65432/api/kas/'; * * const ciphertext = '...'; * const client = new NanoTDFClient( - * { + * await clientSecretAuthProvider({ * clientId: 'tdf-client', * clientSecret: '123-456', * organizationName: 'tdf', * oidcOrigin: OIDC_ENDPOINT, - * }, + * }), * KAS_URL * ); * client.decrypt(ciphertext) @@ -38,7 +38,6 @@ import { TypedArray, createAttribute, Policy } from './tdf/index'; * .catch(err => { * console.error('Some error occurred', err); * }) - * ``` */ export class NanoTDFClient extends Client { /** @@ -57,7 +56,7 @@ export class NanoTDFClient extends Client { // TODO: The version number should be fetched from the API const version = '0.0.1'; // Rewrap key on every request - await this.rewrapKey( + const ukey = await this.rewrapKey( nanotdf.header.toBuffer(), nanotdf.header.getKasRewrapUrl(), nanotdf.header.magicNumberVersion, @@ -65,7 +64,6 @@ export class NanoTDFClient extends Client { nanotdf.header.authTagLength ); - const ukey = this.unwrappedKey; if (!ukey) { throw new Error('Key rewrap failure'); } @@ -88,7 +86,7 @@ export class NanoTDFClient extends Client { const legacyVersion = '0.0.0'; // Rewrap key on every request - await this.rewrapKey( + const key = await this.rewrapKey( nanotdf.header.toBuffer(), nanotdf.header.getKasRewrapUrl(), nanotdf.header.magicNumberVersion, @@ -96,7 +94,6 @@ export class NanoTDFClient extends Client { nanotdf.header.authTagLength ); - const key = this.unwrappedKey; if (!key) { throw new Error('Failed unwrap'); } @@ -168,29 +165,26 @@ export class NanoTDFClient extends Client { * * * @example - * import NanoTDFDatasetClient from 'nanotdf-sdk'; + * ``` + * import { clientSecretAuthProvider, NanoTDFDatasetClient } from '@opentdf/client'; * - * const OIDC_ENDPOINT = 'http://localhost:65432/keycloak/'; - * const KAS_URL = 'http://localhost:65432/kas'; + * const OIDC_ENDPOINT = 'http://localhost:65432/auth/'; + * const KAS_URL = 'http://localhost:65432/api/kas/'; * - * const ciphertext = '...'; - * const client = new NanoTDFDatasetClient.default( - * { - * clientId: 'tdf-client', - * clientSecret: '123-456', - * organizationName: 'tdf', - * exchange: 'client', - * oidcOrigin: OIDC_ENDPOINT, - * }, - * KAS_URL - * ); - * client.decrypt(ciphertext) - * .then(plaintext => { - * console.log('Plaintext', plaintext); - * }) - * .catch(err => { - * console.error('Some error occurred', err); - * }) + * const ciphertext = '...'; + * const client = new NanoTDFDatasetClient.default( + * await clientSecretAuthProvider({ + * clientId: 'tdf-client', + * clientSecret: '123-456', + * organizationName: 'tdf', + * exchange: 'client', + * oidcOrigin: OIDC_ENDPOINT, + * }), + * KAS_URL + * ); + * const plaintext = client.decrypt(ciphertext); + * console.log('Plaintext', plaintext); + * ``` */ export class NanoTDFDatasetClient extends Client { // Total unique IVs(2^24 -1) used for encrypting the nano tdf payloads @@ -199,7 +193,8 @@ export class NanoTDFDatasetClient extends Client { private maxKeyIteration: number; private keyIterationCount: number; - private cachedEphemmeralKey?: Uint8Array; + private cachedEphemeralKey?: Uint8Array; + private unwrappedKey?: CryptoKey; private symmetricKey?: CryptoKey; private cachedHeader?: Header; @@ -345,13 +340,13 @@ export class NanoTDFDatasetClient extends Client { // Parse ciphertext const nanotdf = NanoTDF.from(ciphertext); - if (!this.cachedEphemmeralKey) { + if (!this.cachedEphemeralKey) { // First decrypt return this.rewrapAndDecrypt(nanotdf); } // Other encrypts - if (this.cachedEphemmeralKey.toString() == nanotdf.header.ephemeralPublicKey.toString()) { + if (this.cachedEphemeralKey.toString() == nanotdf.header.ephemeralPublicKey.toString()) { const ukey = this.unwrappedKey; if (!ukey) { throw new Error('Key rewrap failure'); @@ -369,19 +364,19 @@ export class NanoTDFDatasetClient extends Client { const version = '0.0.1'; // Rewrap key on every request - await this.rewrapKey( + const ukey = await this.rewrapKey( nanotdf.header.toBuffer(), nanotdf.header.getKasRewrapUrl(), nanotdf.header.magicNumberVersion, version, nanotdf.header.authTagLength ); - const ukey = this.unwrappedKey; if (!ukey) { throw new Error('Key rewrap failure'); } - this.cachedEphemmeralKey = nanotdf.header.ephemeralPublicKey; + this.cachedEphemeralKey = nanotdf.header.ephemeralPublicKey; + this.unwrappedKey = ukey; // Return decrypt promise return decrypt(ukey, nanotdf); diff --git a/lib/src/nanotdf-crypto/index.ts b/lib/src/nanotdf-crypto/index.ts index 88ed0fa5..d10d2803 100644 --- a/lib/src/nanotdf-crypto/index.ts +++ b/lib/src/nanotdf-crypto/index.ts @@ -6,7 +6,7 @@ export { default as digest } from './digest'; export { default as encrypt } from './encrypt'; export { default as generateKeyPair } from './generateKeyPair'; export { default as importRawKey } from './importRawKey'; -export { default as keyAgreement } from './keyAgreement'; +export { keyAgreement } from './keyAgreement'; export { default as exportCryptoKey } from './exportCryptoKey'; export { default as generateRandomNumber } from './generateRandomNumber'; export { default as pemPublicToCrypto, extractPublicFromCertToCrypto } from './pemPublicToCrypto'; diff --git a/lib/src/nanotdf-crypto/keyAgreement.ts b/lib/src/nanotdf-crypto/keyAgreement.ts index 8373a4e2..574b5b04 100644 --- a/lib/src/nanotdf-crypto/keyAgreement.ts +++ b/lib/src/nanotdf-crypto/keyAgreement.ts @@ -50,7 +50,7 @@ interface KeyAgreementOptions { * - privateKey {CryptoKey} default: "undefined" * - options {Object} default: { bitLength: 256, hkdfHash: 'SHA-512', hkdfSalt: "new UInt8Array()", hkdfInfo: "new UInt8Array()", keyCipher: 'AES-GCM', keyLength: 256, keyUsages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], isExtractable: true } */ -export default async function keyAgreement( +export async function keyAgreement( privateKey: CryptoKey, publicKey: CryptoKey, options: Partial = { diff --git a/lib/src/nanotdf/Client.ts b/lib/src/nanotdf/Client.ts index 19d97c5f..23e29cb3 100644 --- a/lib/src/nanotdf/Client.ts +++ b/lib/src/nanotdf/Client.ts @@ -65,7 +65,6 @@ export default class Client { dataAttributes: string[] = []; protected ephemeralKeyPair?: Required>; protected requestSignerKeyPair?: Required>; - protected unwrappedKey?: CryptoKey; protected iv?: number; /** @@ -130,15 +129,6 @@ export default class Client { return { publicKey, privateKey }; } - /** - * Get the unwrapped key - * - * Returns the unwrapped key or undefined if not rewrapped - */ - getUnwrappedKey(): CryptoKey | undefined { - return this.unwrappedKey; - } - /** * Add attribute to the TDF file/data * @@ -285,8 +275,9 @@ export default class Client { } // UnwrappedKey + let unwrappedKey; try { - this.unwrappedKey = await importRawKey( + unwrappedKey = await importRawKey( decryptedKey, // Want to use the key to encrypt and decrypt. Signing key will be used later. [KeyUsageType.Encrypt, KeyUsageType.Decrypt], @@ -299,7 +290,7 @@ export default class Client { throw new Error(`Unable to import raw key.\n Caused by: ${e.message}`); } - return this.unwrappedKey; + return unwrappedKey; } catch (e) { throw new Error(`Could not rewrap key with entity object.\n Caused by: ${e.message}`); } diff --git a/lib/src/version.ts b/lib/src/version.ts index 96740fca..a5d9bfdc 100644 --- a/lib/src/version.ts +++ b/lib/src/version.ts @@ -1,7 +1,7 @@ /** * Exposes the released version number of the `@opentdf/client` package */ -export const version = process.env.PKG_VERSION; +export const version = typeof process === 'undefined' ? 'main' : process.env.PKG_VERSION; /** * A string name used to label requests as coming from this library client. diff --git a/lib/test/nano-roundtrip.test.ts b/lib/test/nano-roundtrip.test.ts new file mode 100644 index 00000000..bfe33544 --- /dev/null +++ b/lib/test/nano-roundtrip.test.ts @@ -0,0 +1,63 @@ +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import { AuthProvider } from '../src/auth/auth.js'; + +import { NanoTDFClient } from '../src/index.js'; + +const authProvider = { + updateClientPublicKey: async (clientPubKey) => { + /* mocked function */ + }, + authorization: async () => + 'Bearer dummy-auth-token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZGYiLCJzdWIiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.XFu4sQxAd6n-b7urqTdQ-I9zKqKSQtC04unHsMSpJjc', +}; + +const kasPubKey = `-----BEGIN CERTIFICATE----- +MIIBcjCCARegAwIBAgIUHeLWauo8LkzzWtq3alLWzR9Gxt4wCgYIKoZIzj0EAwIw +DjEMMAoGA1UEAwwDa2FzMB4XDTIyMDMwMzE0NDcwMFoXDTIzMDMwMzE0NDcwMFow +DjEMMAoGA1UEAwwDa2FzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEoPCmh6d +boMZLjFLX6Q3gLsiV40e1fpJ9gfm8GXogh0w7c8bb3SkNH4NXZ0YpevkScy2UZOl +KIgSAn70lERIj6NTMFEwHQYDVR0OBBYEFAHmNbtdve05mTWKmHLiTSG49oL1MB8G +A1UdIwQYMBaAFAHmNbtdve05mTWKmHLiTSG49oL1MA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSQAwRgIhALJmwqc6xYQKu84GOjz+P4WmBHEoGvqT2ZWXnLuZ +5jKMAiEAmdiam2/jiDTt38PKQkkJvqTlOogTMnigGBOE+FuuB2M= +-----END CERTIFICATE-----`; + +function mockApiResponse(status = 200, body = {}) { + return new globalThis.Response(JSON.stringify(body), { + status, + headers: { 'Content-type': 'application/json' }, + }); +} + +function initSandbox() { + const sandbox = sinon.createSandbox(); + const fetchLives = sandbox.stub(globalThis, 'fetch'); + fetchLives.callsFake(async (resource, init) => { + if (resource === 'http://localhost:65432/api/kas/kas_public_key?algorithm=ec:secp256r1') { + return mockApiResponse(200, kasPubKey); + } + console.log(`trying to fetch( resource: [${resource}], init:`, init); + return mockApiResponse(404); + }); + return sandbox; +} + +const kasUrl = 'http://localhost:65432/api/kas'; + +describe('Local roundtrip Tests', () => { + it('roundtrip string', async () => { + // const sandbox = initSandbox(); + const sandbox = initSandbox(); + try { + const client = new NanoTDFClient(authProvider, kasUrl); + const keyAgreementSpy = sandbox.spy(globalThis.crypto.subtle, 'deriveKey'); + sandbox.stub(client, 'rewrapKey').callsFake(async () => keyAgreementSpy.lastCall.returnValue); + const cipherText = await client.encrypt('hello world'); + const actual = await client.decrypt(cipherText); + expect(new TextDecoder().decode(actual)).to.be.equal('hello world'); + } finally { + sandbox.reset(); + } + }); +}); diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 8eeb261f..00d53d27 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -9,7 +9,7 @@ "declarationMap": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "inlineSourceMap": false, + "inlineSourceMap": true, "lib": ["dom", "dom.iterable", "es2020"], "module": "esnext", "moduleResolution": "node",