Skip to content

Commit

Permalink
Merge branch 'main' into feature/kao-version-info
Browse files Browse the repository at this point in the history
  • Loading branch information
dmihalcik-virtru authored Jan 23, 2025
2 parents b31d256 + 11ad526 commit 041eefd
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 113 deletions.
7 changes: 2 additions & 5 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
54 changes: 1 addition & 53 deletions lib/tdf3/src/models/key-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,59 +63,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<KeyAccessObject> {
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),
},
schemaVersion,
};
if (this.kid) {
this.keyAccessObject.kid = this.kid;
}
return this.keyAccessObject;
}
}

export type KeyAccess = Remote | Wrapped;
export type KeyAccess = Wrapped;

/**
* A KeyAccess object stores all information about how an object key OR one key split is stored.
Expand Down
122 changes: 67 additions & 55 deletions lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
KeyInfo,
Manifest,
Policy,
Remote as KeyAccessRemote,
SplitKey,
Wrapped as KeyAccessWrapped,
KeyAccess,
Expand Down Expand Up @@ -81,8 +80,8 @@ export type BuildKeyAccess = {

type Segment = {
hash: string;
segmentSize: number | undefined;
encryptedSegmentSize: number | undefined;
segmentSize?: number;
encryptedSegmentSize?: number;
};

type EntryInfo = {
Expand All @@ -92,14 +91,32 @@ type EntryInfo = {
fileByteCount?: number;
};

type Mailbox<T> = Promise<T> & {
set: (value: T) => void;
reject: (error: Error) => void;
};

function mailbox<T>(): Mailbox<T> {
let set: (value: T) => void;
let reject: (error: Error) => void;

const promise = new Promise<T>((resolve, rejectFn) => {
set = resolve;
reject = rejectFn;
}) as Mailbox<T>;

promise.set = set!;
promise.reject = reject!;

return promise;
}

type Chunk = {
hash: string;
plainSegmentSize?: number;
encryptedOffset: number;
encryptedSegmentSize?: number;
decryptedChunk?: null | DecryptResult;
promise: Promise<unknown>;
_resolve?: (value: unknown) => void;
_reject?: (value: unknown) => void;
decryptedChunk: Mailbox<DecryptResult>;
};

export type IntegrityAlgorithm = 'GMAC' | 'HS256';
Expand Down Expand Up @@ -219,10 +236,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`);
}
}

Expand Down Expand Up @@ -726,10 +741,10 @@ async function decryptChunk(
hash: string,
cipher: SymmetricCipher,
segmentIntegrityAlgorithm: IntegrityAlgorithm,
cryptoService: CryptoService,
isLegacyTDF: boolean
): Promise<DecryptResult> {
if (segmentIntegrityAlgorithm !== 'GMAC' && segmentIntegrityAlgorithm !== 'HS256') {
throw new UnsupportedError(`Unsupported integrity alg [${segmentIntegrityAlgorithm}]`);
}
const segmentSig = await getSignature(
new Uint8Array(reconstructedKeyBinary.asArrayBuffer()),
Expand Down Expand Up @@ -809,7 +824,6 @@ export async function sliceAndDecrypt({
reconstructedKeyBinary,
slice,
cipher,
cryptoService,
segmentIntegrityAlgorithm,
isLegacyTDF,
}: {
Expand All @@ -822,34 +836,35 @@ 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;
const encryptedChunk = new Uint8Array(
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,
reconstructedKeyBinary,
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);
}
}
}
Expand All @@ -872,6 +887,7 @@ export async function readStream(cfg: DecryptConfiguration) {
encryptedSegmentSizeDefault: defaultSegmentSize,
rootSignature,
segmentHashAlg,
segmentSizeDefault,
segments,
} = manifest.encryptionInformation.integrityInformation;
const { metadata, reconstructedKeyBinary } = await unwrapKey({
Expand Down Expand Up @@ -908,14 +924,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
Expand All @@ -934,27 +942,36 @@ 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 }) => {
const result = (() => {
let _resolve, _reject;
const chunk: Chunk = {
hash,
encryptedOffset: mapOfRequestsOffset,
encryptedSegmentSize,
promise: new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
}),
};
chunk._resolve = _resolve;
chunk._reject = _reject;
return chunk;
})();
mapOfRequestsOffset += encryptedSegmentSize || encryptedSegmentSizeDefault;
return [hash, result];
})
segments.map(
({
hash,
encryptedSegmentSize = encryptedSegmentSizeDefault,
segmentSize = segmentSizeDefault,
}) => {
const result = (() => {
const chunk: Chunk = {
hash,
encryptedOffset: mapOfRequestsOffset,
encryptedSegmentSize,
decryptedChunk: mailbox<DecryptResult>(),
plainSegmentSize: segmentSize,
};
return chunk;
})();
mapOfRequestsOffset += encryptedSegmentSize;
return [hash, result];
}
)
);

const cipher = new AesGcmCipher(cfg.cryptoService);
Expand Down Expand Up @@ -984,16 +1001,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 }),
Expand Down
38 changes: 38 additions & 0 deletions lib/tests/mocha/unit/tdf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -98,6 +99,43 @@ describe('fetchKasPublicKey', async () => {
});
});

describe('validatePolicyObject', () => {
const testCases: { title: string; policy: Partial<Policy>; 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[] = [
Expand Down

0 comments on commit 041eefd

Please sign in to comment.