Skip to content

Commit 041eefd

Browse files
Merge branch 'main' into feature/kao-version-info
2 parents b31d256 + 11ad526 commit 041eefd

File tree

4 files changed

+108
-113
lines changed

4 files changed

+108
-113
lines changed

cli/src/cli.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,8 @@ export const handleArgs = (args: string[]) => {
562562
log('DEBUG', `About to TDF3 decrypt [${argv.file}]`);
563563
const ct = await client.read(await parseReadOptions(argv));
564564
const destination = argv.output ? createWriteStream(argv.output) : process.stdout;
565-
try {
566-
await ct.pipeTo(Writable.toWeb(destination));
567-
} catch (e) {
568-
log('ERROR', `Failed to pipe to destination stream: ${e}`);
569-
}
565+
await ct.pipeTo(Writable.toWeb(destination));
566+
570567
const lastRequest = authProvider.requestLog[authProvider.requestLog.length - 1];
571568
log('SILLY', `last request is ${JSON.stringify(lastRequest)}`);
572569
let accessToken = null;

lib/tdf3/src/models/key-access.ts

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -63,59 +63,7 @@ export class Wrapped {
6363
}
6464
}
6565

66-
export class Remote {
67-
readonly type = 'remote';
68-
keyAccessObject?: KeyAccessObject;
69-
wrappedKey?: string;
70-
policyBinding?: string;
71-
72-
constructor(
73-
public readonly url: string,
74-
public readonly kid: string | undefined,
75-
public readonly publicKey: string,
76-
public readonly metadata: unknown,
77-
public readonly sid: string
78-
) {}
79-
80-
async write(
81-
policy: Policy,
82-
keyBuffer: Uint8Array,
83-
encryptedMetadataStr: string
84-
): Promise<KeyAccessObject> {
85-
const policyStr = JSON.stringify(policy);
86-
const policyBinding = await cryptoService.hmac(
87-
hex.encodeArrayBuffer(keyBuffer),
88-
base64.encode(policyStr)
89-
);
90-
const unwrappedKeyBinary = Binary.fromArrayBuffer(keyBuffer.buffer);
91-
const wrappedKeyBinary = await cryptoService.encryptWithPublicKey(
92-
unwrappedKeyBinary,
93-
this.publicKey
94-
);
95-
96-
// this.wrappedKey = wrappedKeyBinary.asBuffer().toString('hex');
97-
this.wrappedKey = base64.encode(wrappedKeyBinary.asString());
98-
99-
this.keyAccessObject = {
100-
type: 'remote',
101-
url: this.url,
102-
protocol: 'kas',
103-
wrappedKey: this.wrappedKey,
104-
encryptedMetadata: base64.encode(encryptedMetadataStr),
105-
policyBinding: {
106-
alg: 'HS256',
107-
hash: base64.encode(policyBinding),
108-
},
109-
schemaVersion,
110-
};
111-
if (this.kid) {
112-
this.keyAccessObject.kid = this.kid;
113-
}
114-
return this.keyAccessObject;
115-
}
116-
}
117-
118-
export type KeyAccess = Remote | Wrapped;
66+
export type KeyAccess = Wrapped;
11967

12068
/**
12169
* A KeyAccess object stores all information about how an object key OR one key split is stored.

lib/tdf3/src/tdf.ts

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
KeyInfo,
3636
Manifest,
3737
Policy,
38-
Remote as KeyAccessRemote,
3938
SplitKey,
4039
Wrapped as KeyAccessWrapped,
4140
KeyAccess,
@@ -81,8 +80,8 @@ export type BuildKeyAccess = {
8180

8281
type Segment = {
8382
hash: string;
84-
segmentSize: number | undefined;
85-
encryptedSegmentSize: number | undefined;
83+
segmentSize?: number;
84+
encryptedSegmentSize?: number;
8685
};
8786

8887
type EntryInfo = {
@@ -92,14 +91,32 @@ type EntryInfo = {
9291
fileByteCount?: number;
9392
};
9493

94+
type Mailbox<T> = Promise<T> & {
95+
set: (value: T) => void;
96+
reject: (error: Error) => void;
97+
};
98+
99+
function mailbox<T>(): Mailbox<T> {
100+
let set: (value: T) => void;
101+
let reject: (error: Error) => void;
102+
103+
const promise = new Promise<T>((resolve, rejectFn) => {
104+
set = resolve;
105+
reject = rejectFn;
106+
}) as Mailbox<T>;
107+
108+
promise.set = set!;
109+
promise.reject = reject!;
110+
111+
return promise;
112+
}
113+
95114
type Chunk = {
96115
hash: string;
116+
plainSegmentSize?: number;
97117
encryptedOffset: number;
98118
encryptedSegmentSize?: number;
99-
decryptedChunk?: null | DecryptResult;
100-
promise: Promise<unknown>;
101-
_resolve?: (value: unknown) => void;
102-
_reject?: (value: unknown) => void;
119+
decryptedChunk: Mailbox<DecryptResult>;
103120
};
104121

105122
export type IntegrityAlgorithm = 'GMAC' | 'HS256';
@@ -219,10 +236,8 @@ export async function buildKeyAccess({
219236
switch (type) {
220237
case 'wrapped':
221238
return new KeyAccessWrapped(kasUrl, kasKeyIdentifier, pubKey, metadata, sid);
222-
case 'remote':
223-
return new KeyAccessRemote(kasUrl, kasKeyIdentifier, pubKey, metadata, sid);
224239
default:
225-
throw new ConfigurationError(`buildKeyAccess: Key access type ${type} is unknown`);
240+
throw new ConfigurationError(`buildKeyAccess: Key access type [${type}] is unsupported`);
226241
}
227242
}
228243

@@ -726,10 +741,10 @@ async function decryptChunk(
726741
hash: string,
727742
cipher: SymmetricCipher,
728743
segmentIntegrityAlgorithm: IntegrityAlgorithm,
729-
cryptoService: CryptoService,
730744
isLegacyTDF: boolean
731745
): Promise<DecryptResult> {
732746
if (segmentIntegrityAlgorithm !== 'GMAC' && segmentIntegrityAlgorithm !== 'HS256') {
747+
throw new UnsupportedError(`Unsupported integrity alg [${segmentIntegrityAlgorithm}]`);
733748
}
734749
const segmentSig = await getSignature(
735750
new Uint8Array(reconstructedKeyBinary.asArrayBuffer()),
@@ -809,7 +824,6 @@ export async function sliceAndDecrypt({
809824
reconstructedKeyBinary,
810825
slice,
811826
cipher,
812-
cryptoService,
813827
segmentIntegrityAlgorithm,
814828
isLegacyTDF,
815829
}: {
@@ -822,34 +836,35 @@ export async function sliceAndDecrypt({
822836
isLegacyTDF: boolean;
823837
}) {
824838
for (const index in slice) {
825-
const { encryptedOffset, encryptedSegmentSize, _resolve, _reject } = slice[index];
839+
const { encryptedOffset, encryptedSegmentSize, plainSegmentSize } = slice[index];
826840

827841
const offset =
828842
slice[0].encryptedOffset === 0 ? encryptedOffset : encryptedOffset % slice[0].encryptedOffset;
829843
const encryptedChunk = new Uint8Array(
830844
buffer.slice(offset, offset + (encryptedSegmentSize as number))
831845
);
832846

847+
if (encryptedChunk.length !== encryptedSegmentSize) {
848+
throw new DecryptError('Failed to fetch entire segment');
849+
}
850+
833851
try {
834852
const result = await decryptChunk(
835853
encryptedChunk,
836854
reconstructedKeyBinary,
837855
slice[index]['hash'],
838856
cipher,
839857
segmentIntegrityAlgorithm,
840-
cryptoService,
841858
isLegacyTDF
842859
);
843-
slice[index].decryptedChunk = result;
844-
if (_resolve) {
845-
_resolve(null);
860+
if (plainSegmentSize && result.payload.length() !== plainSegmentSize) {
861+
throw new DecryptError(
862+
`incorrect segment size: found [${result.payload.length()}], expected [${plainSegmentSize}]`
863+
);
846864
}
865+
slice[index].decryptedChunk.set(result);
847866
} catch (e) {
848-
if (_reject) {
849-
_reject(e);
850-
} else {
851-
throw e;
852-
}
867+
slice[index].decryptedChunk.reject(e);
853868
}
854869
}
855870
}
@@ -872,6 +887,7 @@ export async function readStream(cfg: DecryptConfiguration) {
872887
encryptedSegmentSizeDefault: defaultSegmentSize,
873888
rootSignature,
874889
segmentHashAlg,
890+
segmentSizeDefault,
875891
segments,
876892
} = manifest.encryptionInformation.integrityInformation;
877893
const { metadata, reconstructedKeyBinary } = await unwrapKey({
@@ -908,14 +924,6 @@ export async function readStream(cfg: DecryptConfiguration) {
908924
integrityAlgorithm
909925
);
910926

911-
const rootSig = isLegacyTDF
912-
? base64.encode(hex.encodeArrayBuffer(payloadSig))
913-
: base64.encodeArrayBuffer(payloadSig);
914-
915-
if (manifest.encryptionInformation.integrityInformation.rootSignature.sig !== rootSig) {
916-
throw new IntegrityError('Failed integrity check on root signature');
917-
}
918-
919927
if (!cfg.noVerifyAssertions) {
920928
for (const assertion of manifest.assertions || []) {
921929
// Create a default assertion key
@@ -934,27 +942,36 @@ export async function readStream(cfg: DecryptConfiguration) {
934942
}
935943
}
936944

945+
const rootSig = isLegacyTDF
946+
? base64.encode(hex.encodeArrayBuffer(payloadSig))
947+
: base64.encodeArrayBuffer(payloadSig);
948+
949+
if (manifest.encryptionInformation.integrityInformation.rootSignature.sig !== rootSig) {
950+
throw new IntegrityError('Failed integrity check on root signature');
951+
}
952+
937953
let mapOfRequestsOffset = 0;
938954
const chunkMap = new Map(
939-
segments.map(({ hash, encryptedSegmentSize = encryptedSegmentSizeDefault }) => {
940-
const result = (() => {
941-
let _resolve, _reject;
942-
const chunk: Chunk = {
943-
hash,
944-
encryptedOffset: mapOfRequestsOffset,
945-
encryptedSegmentSize,
946-
promise: new Promise((resolve, reject) => {
947-
_resolve = resolve;
948-
_reject = reject;
949-
}),
950-
};
951-
chunk._resolve = _resolve;
952-
chunk._reject = _reject;
953-
return chunk;
954-
})();
955-
mapOfRequestsOffset += encryptedSegmentSize || encryptedSegmentSizeDefault;
956-
return [hash, result];
957-
})
955+
segments.map(
956+
({
957+
hash,
958+
encryptedSegmentSize = encryptedSegmentSizeDefault,
959+
segmentSize = segmentSizeDefault,
960+
}) => {
961+
const result = (() => {
962+
const chunk: Chunk = {
963+
hash,
964+
encryptedOffset: mapOfRequestsOffset,
965+
encryptedSegmentSize,
966+
decryptedChunk: mailbox<DecryptResult>(),
967+
plainSegmentSize: segmentSize,
968+
};
969+
return chunk;
970+
})();
971+
mapOfRequestsOffset += encryptedSegmentSize;
972+
return [hash, result];
973+
}
974+
)
958975
);
959976

960977
const cipher = new AesGcmCipher(cfg.cryptoService);
@@ -984,16 +1001,11 @@ export async function readStream(cfg: DecryptConfiguration) {
9841001
}
9851002

9861003
const [hash, chunk] = chunkMap.entries().next().value;
987-
if (!chunk.decryptedChunk) {
988-
await chunk.promise;
989-
}
990-
const decryptedSegment = chunk.decryptedChunk;
1004+
const decryptedSegment = await chunk.decryptedChunk;
9911005

9921006
controller.enqueue(new Uint8Array(decryptedSegment.payload.asByteArray()));
9931007
progress += chunk.encryptedSegmentSize;
9941008
cfg.progressHandler?.(progress);
995-
996-
chunk.decryptedChunk = null;
9971009
chunkMap.delete(hash);
9981010
},
9991011
...(cfg.fileStreamServiceWorker && { fileStreamServiceWorker: cfg.fileStreamServiceWorker }),

lib/tests/mocha/unit/tdf.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22

33
import * as TDF from '../../../tdf3/src/tdf.js';
44
import { KeyAccessObject } from '../../../tdf3/src/models/key-access.js';
5+
import { PolicyBody, type Policy } from '../../../tdf3/src/models/policy.js';
56
import { OriginAllowList } from '../../../src/access.js';
67
import { ConfigurationError, InvalidFileError, UnsafeUrlError } from '../../../src/errors.js';
78

@@ -98,6 +99,43 @@ describe('fetchKasPublicKey', async () => {
9899
});
99100
});
100101

102+
describe('validatePolicyObject', () => {
103+
const testCases: { title: string; policy: Partial<Policy>; error?: string }[] = [
104+
{
105+
title: 'missing uuid',
106+
policy: { body: { dataAttributes: [], dissem: ['someDissem'] } },
107+
error: 'uuid',
108+
},
109+
{
110+
title: 'missing body',
111+
policy: { uuid: 'someUuid' },
112+
error: 'body',
113+
},
114+
{
115+
title: 'missing body.dissem',
116+
policy: { uuid: 'someUuid', body: {} as PolicyBody },
117+
error: 'dissem',
118+
},
119+
{
120+
title: 'valid policy',
121+
policy: { uuid: 'someUuid', body: { dataAttributes: [], dissem: ['someDissem'] } },
122+
},
123+
];
124+
125+
testCases.forEach(({ title, policy, error }) => {
126+
it(`should handle ${title}`, () => {
127+
if (error) {
128+
expect(() => TDF.validatePolicyObject(policy as Policy)).to.throw(
129+
ConfigurationError,
130+
error
131+
);
132+
} else {
133+
expect(() => TDF.validatePolicyObject(policy as Policy)).to.not.throw();
134+
}
135+
});
136+
});
137+
});
138+
101139
describe('splitLookupTableFactory', () => {
102140
it('should return a correct split table for valid input', () => {
103141
const keyAccess: KeyAccessObject[] = [

0 commit comments

Comments
 (0)