From 22781ce251249e072889f1df7d8f58e0f329f239 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Wed, 22 Jan 2025 14:43:17 -0500 Subject: [PATCH 1/5] chore(core): Remove KeyAccessRemote this is currently unsupported in opentdf --- lib/tdf3/src/models/key-access.ts | 53 +------------------------------ lib/tdf3/src/tdf.ts | 5 +-- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/lib/tdf3/src/models/key-access.ts b/lib/tdf3/src/models/key-access.ts index efc8b1da..90872e75 100644 --- a/lib/tdf3/src/models/key-access.ts +++ b/lib/tdf3/src/models/key-access.ts @@ -60,58 +60,7 @@ export class Wrapped { } } -export class Remote { - readonly type = 'remote'; - keyAccessObject?: KeyAccessObject; - wrappedKey?: string; - policyBinding?: string; - - constructor( - public readonly url: string, - public readonly kid: string | undefined, - public readonly publicKey: string, - public readonly metadata: unknown, - public readonly sid: string - ) {} - - async write( - policy: Policy, - keyBuffer: Uint8Array, - encryptedMetadataStr: string - ): Promise { - const policyStr = JSON.stringify(policy); - const policyBinding = await cryptoService.hmac( - hex.encodeArrayBuffer(keyBuffer), - base64.encode(policyStr) - ); - const unwrappedKeyBinary = Binary.fromArrayBuffer(keyBuffer.buffer); - const wrappedKeyBinary = await cryptoService.encryptWithPublicKey( - unwrappedKeyBinary, - this.publicKey - ); - - // this.wrappedKey = wrappedKeyBinary.asBuffer().toString('hex'); - this.wrappedKey = base64.encode(wrappedKeyBinary.asString()); - - this.keyAccessObject = { - type: 'remote', - url: this.url, - protocol: 'kas', - wrappedKey: this.wrappedKey, - encryptedMetadata: base64.encode(encryptedMetadataStr), - policyBinding: { - alg: 'HS256', - hash: base64.encode(policyBinding), - }, - }; - if (this.kid) { - this.keyAccessObject.kid = this.kid; - } - return this.keyAccessObject; - } -} - -export type KeyAccess = Remote | Wrapped; +export type KeyAccess = Wrapped; export type KeyAccessObject = { sid?: string; diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index ee405c86..e95211bc 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -35,7 +35,6 @@ import { KeyInfo, Manifest, Policy, - Remote as KeyAccessRemote, SplitKey, Wrapped as KeyAccessWrapped, KeyAccess, @@ -219,10 +218,8 @@ export async function buildKeyAccess({ switch (type) { case 'wrapped': return new KeyAccessWrapped(kasUrl, kasKeyIdentifier, pubKey, metadata, sid); - case 'remote': - return new KeyAccessRemote(kasUrl, kasKeyIdentifier, pubKey, metadata, sid); default: - throw new ConfigurationError(`buildKeyAccess: Key access type ${type} is unknown`); + throw new ConfigurationError(`buildKeyAccess: Key access type [${type}] is unsupported`); } } From b820265a3387946f9ccf09a0276681ebd794e892 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Wed, 22 Jan 2025 16:57:26 -0500 Subject: [PATCH 2/5] fix(sdk): Be more picky about things --- lib/tdf3/src/tdf.ts | 73 +++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index e95211bc..687b2bca 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -80,8 +80,8 @@ export type BuildKeyAccess = { type Segment = { hash: string; - segmentSize: number | undefined; - encryptedSegmentSize: number | undefined; + segmentSize?: number; + encryptedSegmentSize?: number; }; type EntryInfo = { @@ -91,14 +91,32 @@ type EntryInfo = { fileByteCount?: number; }; +type Mailbox = Promise & { + set: (value: T) => void; + reject: (error: Error) => void; +}; + +function mailbox(): Mailbox { + let set: (value: T) => void; + let reject: (error: Error) => void; + + const promise = new Promise((resolve, rejectFn) => { + set = resolve; + reject = rejectFn; + }) as Mailbox; + + promise.set = set!; + promise.reject = reject!; + + return promise; +} + type Chunk = { hash: string; + plainSegmentSize?: number; encryptedOffset: number; encryptedSegmentSize?: number; - decryptedChunk?: null | DecryptResult; - promise: Promise; - _resolve?: (value: unknown) => void; - _reject?: (value: unknown) => void; + decryptedChunk: Mailbox; }; export type IntegrityAlgorithm = 'GMAC' | 'HS256'; @@ -723,10 +741,10 @@ async function decryptChunk( hash: string, cipher: SymmetricCipher, segmentIntegrityAlgorithm: IntegrityAlgorithm, - cryptoService: CryptoService, isLegacyTDF: boolean ): Promise { if (segmentIntegrityAlgorithm !== 'GMAC' && segmentIntegrityAlgorithm !== 'HS256') { + throw new UnsupportedError(`Unsupported integrity alg [${segmentIntegrityAlgorithm}]`); } const segmentSig = await getSignature( new Uint8Array(reconstructedKeyBinary.asArrayBuffer()), @@ -806,7 +824,6 @@ export async function sliceAndDecrypt({ reconstructedKeyBinary, slice, cipher, - cryptoService, segmentIntegrityAlgorithm, isLegacyTDF, }: { @@ -819,7 +836,7 @@ export async function sliceAndDecrypt({ isLegacyTDF: boolean; }) { for (const index in slice) { - const { encryptedOffset, encryptedSegmentSize, _resolve, _reject } = slice[index]; + const { encryptedOffset, encryptedSegmentSize, plainSegmentSize } = slice[index]; const offset = slice[0].encryptedOffset === 0 ? encryptedOffset : encryptedOffset % slice[0].encryptedOffset; @@ -827,6 +844,10 @@ export async function sliceAndDecrypt({ buffer.slice(offset, offset + (encryptedSegmentSize as number)) ); + if (encryptedChunk.length !== encryptedSegmentSize) { + throw new DecryptError('Failed to fetch entire segment'); + } + try { const result = await decryptChunk( encryptedChunk, @@ -834,19 +855,14 @@ export async function sliceAndDecrypt({ slice[index]['hash'], cipher, segmentIntegrityAlgorithm, - cryptoService, isLegacyTDF ); - slice[index].decryptedChunk = result; - if (_resolve) { - _resolve(null); + if (plainSegmentSize && result.payload.length() !== plainSegmentSize) { + throw new DecryptError(`incorrect segment size: found [${result.payload.length()}], expected [${plainSegmentSize}]`); } + slice[index].decryptedChunk.set(result); } catch (e) { - if (_reject) { - _reject(e); - } else { - throw e; - } + slice[index].decryptedChunk.reject(e); } } } @@ -869,6 +885,7 @@ export async function readStream(cfg: DecryptConfiguration) { encryptedSegmentSizeDefault: defaultSegmentSize, rootSignature, segmentHashAlg, + segmentSizeDefault, segments, } = manifest.encryptionInformation.integrityInformation; const { metadata, reconstructedKeyBinary } = await unwrapKey({ @@ -933,23 +950,18 @@ export async function readStream(cfg: DecryptConfiguration) { let mapOfRequestsOffset = 0; const chunkMap = new Map( - segments.map(({ hash, encryptedSegmentSize = encryptedSegmentSizeDefault }) => { + segments.map(({ hash, encryptedSegmentSize = encryptedSegmentSizeDefault, segmentSize = segmentSizeDefault }) => { const result = (() => { - let _resolve, _reject; const chunk: Chunk = { hash, encryptedOffset: mapOfRequestsOffset, encryptedSegmentSize, - promise: new Promise((resolve, reject) => { - _resolve = resolve; - _reject = reject; - }), + decryptedChunk: mailbox(), + plainSegmentSize: segmentSize, }; - chunk._resolve = _resolve; - chunk._reject = _reject; return chunk; })(); - mapOfRequestsOffset += encryptedSegmentSize || encryptedSegmentSizeDefault; + mapOfRequestsOffset += encryptedSegmentSize; return [hash, result]; }) ); @@ -981,16 +993,11 @@ export async function readStream(cfg: DecryptConfiguration) { } const [hash, chunk] = chunkMap.entries().next().value; - if (!chunk.decryptedChunk) { - await chunk.promise; - } - const decryptedSegment = chunk.decryptedChunk; + const decryptedSegment = await chunk.decryptedChunk; controller.enqueue(new Uint8Array(decryptedSegment.payload.asByteArray())); progress += chunk.encryptedSegmentSize; cfg.progressHandler?.(progress); - - chunk.decryptedChunk = null; chunkMap.delete(hash); }, ...(cfg.fileStreamServiceWorker && { fileStreamServiceWorker: cfg.fileStreamServiceWorker }), From c880c6bdaa8ac3454c000feec4c74ac1b239e263 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Wed, 22 Jan 2025 17:23:47 -0500 Subject: [PATCH 3/5] fail better --- cli/src/cli.ts | 7 ++----- lib/tdf3/src/tdf.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index f0175f2c..d6a591ee 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -562,11 +562,8 @@ export const handleArgs = (args: string[]) => { log('DEBUG', `About to TDF3 decrypt [${argv.file}]`); const ct = await client.read(await parseReadOptions(argv)); const destination = argv.output ? createWriteStream(argv.output) : process.stdout; - try { - await ct.pipeTo(Writable.toWeb(destination)); - } catch (e) { - log('ERROR', `Failed to pipe to destination stream: ${e}`); - } + await ct.pipeTo(Writable.toWeb(destination)); + const lastRequest = authProvider.requestLog[authProvider.requestLog.length - 1]; log('SILLY', `last request is ${JSON.stringify(lastRequest)}`); let accessToken = null; diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index 687b2bca..35a680a8 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -922,14 +922,6 @@ export async function readStream(cfg: DecryptConfiguration) { integrityAlgorithm ); - const rootSig = isLegacyTDF - ? base64.encode(hex.encodeArrayBuffer(payloadSig)) - : base64.encodeArrayBuffer(payloadSig); - - if (manifest.encryptionInformation.integrityInformation.rootSignature.sig !== rootSig) { - throw new IntegrityError('Failed integrity check on root signature'); - } - if (!cfg.noVerifyAssertions) { for (const assertion of manifest.assertions || []) { // Create a default assertion key @@ -948,6 +940,14 @@ export async function readStream(cfg: DecryptConfiguration) { } } + const rootSig = isLegacyTDF + ? base64.encode(hex.encodeArrayBuffer(payloadSig)) + : base64.encodeArrayBuffer(payloadSig); + + if (manifest.encryptionInformation.integrityInformation.rootSignature.sig !== rootSig) { + throw new IntegrityError('Failed integrity check on root signature'); + } + let mapOfRequestsOffset = 0; const chunkMap = new Map( segments.map(({ hash, encryptedSegmentSize = encryptedSegmentSizeDefault, segmentSize = segmentSizeDefault }) => { From 2ec76c025e0e94b7a673466bcd08c0181b352522 Mon Sep 17 00:00:00 2001 From: dmihalcik-virtru <38867245+dmihalcik-virtru@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:25:20 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A4=96=20=F0=9F=8E=A8=20Autoformat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tdf3/src/tdf.ts | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index 35a680a8..26a24aaf 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -858,7 +858,9 @@ export async function sliceAndDecrypt({ isLegacyTDF ); if (plainSegmentSize && result.payload.length() !== plainSegmentSize) { - throw new DecryptError(`incorrect segment size: found [${result.payload.length()}], expected [${plainSegmentSize}]`); + throw new DecryptError( + `incorrect segment size: found [${result.payload.length()}], expected [${plainSegmentSize}]` + ); } slice[index].decryptedChunk.set(result); } catch (e) { @@ -950,20 +952,26 @@ export async function readStream(cfg: DecryptConfiguration) { let mapOfRequestsOffset = 0; const chunkMap = new Map( - segments.map(({ hash, encryptedSegmentSize = encryptedSegmentSizeDefault, segmentSize = segmentSizeDefault }) => { - const result = (() => { - const chunk: Chunk = { - hash, - encryptedOffset: mapOfRequestsOffset, - encryptedSegmentSize, - decryptedChunk: mailbox(), - plainSegmentSize: segmentSize, - }; - return chunk; - })(); - mapOfRequestsOffset += encryptedSegmentSize; - return [hash, result]; - }) + segments.map( + ({ + hash, + encryptedSegmentSize = encryptedSegmentSizeDefault, + segmentSize = segmentSizeDefault, + }) => { + const result = (() => { + const chunk: Chunk = { + hash, + encryptedOffset: mapOfRequestsOffset, + encryptedSegmentSize, + decryptedChunk: mailbox(), + plainSegmentSize: segmentSize, + }; + return chunk; + })(); + mapOfRequestsOffset += encryptedSegmentSize; + return [hash, result]; + } + ) ); const cipher = new AesGcmCipher(cfg.cryptoService); From 073ca0cb1ea4fa753df543a042d0f9c8934f6fea Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Thu, 23 Jan 2025 09:24:28 -0500 Subject: [PATCH 5/5] Update tdf.spec.ts --- lib/tests/mocha/unit/tdf.spec.ts | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/tests/mocha/unit/tdf.spec.ts b/lib/tests/mocha/unit/tdf.spec.ts index 4a7b01e5..0767f5fb 100644 --- a/lib/tests/mocha/unit/tdf.spec.ts +++ b/lib/tests/mocha/unit/tdf.spec.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import * as TDF from '../../../tdf3/src/tdf.js'; import { KeyAccessObject } from '../../../tdf3/src/models/key-access.js'; +import { PolicyBody, type Policy } from '../../../tdf3/src/models/policy.js'; import { OriginAllowList } from '../../../src/access.js'; import { ConfigurationError, InvalidFileError, UnsafeUrlError } from '../../../src/errors.js'; @@ -98,6 +99,43 @@ describe('fetchKasPublicKey', async () => { }); }); +describe('validatePolicyObject', () => { + const testCases: { title: string; policy: Partial; error?: string }[] = [ + { + title: 'missing uuid', + policy: { body: { dataAttributes: [], dissem: ['someDissem'] } }, + error: 'uuid', + }, + { + title: 'missing body', + policy: { uuid: 'someUuid' }, + error: 'body', + }, + { + title: 'missing body.dissem', + policy: { uuid: 'someUuid', body: {} as PolicyBody }, + error: 'dissem', + }, + { + title: 'valid policy', + policy: { uuid: 'someUuid', body: { dataAttributes: [], dissem: ['someDissem'] } }, + }, + ]; + + testCases.forEach(({ title, policy, error }) => { + it(`should handle ${title}`, () => { + if (error) { + expect(() => TDF.validatePolicyObject(policy as Policy)).to.throw( + ConfigurationError, + error + ); + } else { + expect(() => TDF.validatePolicyObject(policy as Policy)).to.not.throw(); + } + }); + }); +}); + describe('splitLookupTableFactory', () => { it('should return a correct split table for valid input', () => { const keyAccess: KeyAccessObject[] = [