Skip to content

Commit ba177df

Browse files
Merge branch 'main' into chore/rm-backend
2 parents 32fe974 + c6cdbef commit ba177df

File tree

5 files changed

+429
-76
lines changed

5 files changed

+429
-76
lines changed

cli/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"extends": "../lib/tsconfig.json",
33
"compilerOptions": {
4-
"lib": ["es2020", "ES2022.Error"],
4+
"lib": ["es2021", "ES2022.Error"],
55
"module": "Node16",
66
"outDir": "dist"
77
},

lib/tdf3/src/tdf.ts

Lines changed: 110 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import axios from 'axios';
1+
import axios, { AxiosError } from 'axios';
22
import { unsigned } from './utils/buffer-crc32.js';
33
import { exportSPKI, importX509 } from 'jose';
44
import { DecoratedReadableStream } from './client/DecoratedReadableStream.js';
5-
import { EntityObject } from '../../src/tdf/EntityObject.js';
5+
import { EntityObject } from '../../src/tdf/index.js';
66
import { pemToCryptoPublicKey, validateSecureUrl } from '../../src/utils.js';
77
import { DecryptParams } from './client/builders.js';
88
import { AssertionConfig, AssertionKey, AssertionVerificationKeys } from './assertions.js';
@@ -926,25 +926,10 @@ async function unwrapKey({
926926
}
927927
const { keyAccess } = manifest.encryptionInformation;
928928
const splitPotentials = splitLookupTableFactory(keyAccess, allowedKases);
929-
930-
let responseMetadata;
931929
const isAppIdProvider = authProvider && isAppIdProviderCheck(authProvider);
932-
// Get key access information to know the KAS URLS
933-
const rewrappedKeys: Uint8Array[] = [];
934-
935-
for (const [splitId, potentials] of Object.entries(splitPotentials)) {
936-
if (!potentials || !Object.keys(potentials).length) {
937-
throw new UnsafeUrlError(
938-
`Unreconstructable key - no valid KAS found for split ${JSON.stringify(splitId)}`,
939-
''
940-
);
941-
}
942930

943-
// If we have multiple ways of getting a value, try the 'best' way
944-
// or maybe retry across all potential ways? Currently, just tries them all
945-
const [keySplitInfo] = Object.values(potentials);
931+
async function tryKasRewrap(keySplitInfo: KeyAccessObject) {
946932
const url = `${keySplitInfo.url}/${isAppIdProvider ? '' : 'v2/'}rewrap`;
947-
948933
const ephemeralEncryptionKeys = await cryptoService.cryptoToPemPair(
949934
await cryptoService.generateKeyPair()
950935
);
@@ -980,58 +965,122 @@ async function unwrapKey({
980965
};
981966
}
982967

983-
// Create a PoP token by signing the body so KAS knows we actually have a private key
984-
// Expires in 60 seconds
985968
const httpReq = await authProvider.withCreds(buildRequest('POST', url, requestBody));
969+
const {
970+
data: { entityWrappedKey, metadata },
971+
} = await axios.post(httpReq.url, httpReq.body, { headers: httpReq.headers });
972+
973+
const key = Binary.fromString(base64.decode(entityWrappedKey));
974+
const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
975+
key,
976+
ephemeralEncryptionKeys.privateKey
977+
);
978+
979+
return {
980+
key: new Uint8Array(decryptedKeyBinary.asByteArray()),
981+
metadata,
982+
};
983+
}
984+
985+
// Get unique split IDs to determine if we have an OR or AND condition
986+
const splitIds = new Set(Object.keys(splitPotentials));
987+
988+
// If we have only one split ID, it's an OR condition
989+
if (splitIds.size === 1) {
990+
const [splitId] = splitIds;
991+
const potentials = splitPotentials[splitId];
986992

987993
try {
988-
// The response from KAS on a rewrap
989-
const {
990-
data: { entityWrappedKey, metadata },
991-
} = await axios.post(httpReq.url, httpReq.body, { headers: httpReq.headers });
992-
responseMetadata = metadata;
993-
const key = Binary.fromString(base64.decode(entityWrappedKey));
994-
const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
995-
key,
996-
ephemeralEncryptionKeys.privateKey
994+
// OR condition: Try all KAS servers for this split, take first success
995+
const result = await Promise.any(
996+
Object.values(potentials).map(async (keySplitInfo) => {
997+
try {
998+
return await tryKasRewrap(keySplitInfo);
999+
} catch (e) {
1000+
// Rethrow with more context
1001+
throw handleRewrapError(e as Error | AxiosError);
1002+
}
1003+
})
9971004
);
998-
rewrappedKeys.push(new Uint8Array(decryptedKeyBinary.asByteArray()));
999-
} catch (e) {
1000-
if (e.response) {
1001-
if (e.response.status >= 500) {
1002-
throw new ServiceError('rewrap failure', e);
1003-
} else if (e.response.status === 403) {
1004-
throw new PermissionDeniedError('rewrap failure', e);
1005-
} else if (e.response.status === 401) {
1006-
throw new UnauthenticatedError('rewrap auth failure', e);
1007-
} else if (e.response.status === 400) {
1008-
throw new InvalidFileError(
1009-
'rewrap bad request; could indicate an invalid policy binding or a configuration error',
1010-
e
1005+
1006+
const reconstructedKey = keyMerge([result.key]);
1007+
return {
1008+
reconstructedKeyBinary: Binary.fromArrayBuffer(reconstructedKey),
1009+
metadata: result.metadata,
1010+
};
1011+
} catch (error) {
1012+
if (error instanceof AggregateError) {
1013+
// All KAS servers failed
1014+
throw error.errors[0]; // Throw the first error since we've already wrapped them
1015+
}
1016+
throw error;
1017+
}
1018+
} else {
1019+
// AND condition: We need successful results from all different splits
1020+
const splitResults = await Promise.all(
1021+
Object.entries(splitPotentials).map(async ([splitId, potentials]) => {
1022+
if (!potentials || !Object.keys(potentials).length) {
1023+
throw new UnsafeUrlError(
1024+
`Unreconstructable key - no valid KAS found for split ${JSON.stringify(splitId)}`,
1025+
''
10111026
);
1012-
} else {
1013-
throw new NetworkError('rewrap server error', e);
10141027
}
1015-
} else if (e.request) {
1016-
throw new NetworkError('rewrap request failure', e);
1017-
} else if (e.name == 'InvalidAccessError' || e.name == 'OperationError') {
1018-
throw new DecryptError('unable to unwrap key from kas', e);
1019-
}
1020-
throw new InvalidFileError(
1021-
`Unable to decrypt the response from KAS: [${e.name}: ${e.message}], response: [${e?.response?.body}]`,
1022-
e
1028+
1029+
try {
1030+
// For each split, try all potential KAS servers until one succeeds
1031+
return await Promise.any(
1032+
Object.values(potentials).map(async (keySplitInfo) => {
1033+
try {
1034+
return await tryKasRewrap(keySplitInfo);
1035+
} catch (e) {
1036+
throw handleRewrapError(e as Error | AxiosError);
1037+
}
1038+
})
1039+
);
1040+
} catch (error) {
1041+
if (error instanceof AggregateError) {
1042+
// All KAS servers for this split failed
1043+
throw error.errors[0]; // Throw the first error since we've already wrapped them
1044+
}
1045+
throw error;
1046+
}
1047+
})
1048+
);
1049+
1050+
// Merge all the split keys
1051+
const reconstructedKey = keyMerge(splitResults.map((r) => r.key));
1052+
return {
1053+
reconstructedKeyBinary: Binary.fromArrayBuffer(reconstructedKey),
1054+
metadata: splitResults[0].metadata, // Use metadata from first split
1055+
};
1056+
}
1057+
}
1058+
1059+
function handleRewrapError(error: Error | AxiosError) {
1060+
if (axios.isAxiosError(error)) {
1061+
if (error.response?.status && error.response?.status >= 500) {
1062+
return new ServiceError('rewrap failure', error);
1063+
} else if (error.response?.status === 403) {
1064+
return new PermissionDeniedError('rewrap failure', error);
1065+
} else if (error.response?.status === 401) {
1066+
return new UnauthenticatedError('rewrap auth failure', error);
1067+
} else if (error.response?.status === 400) {
1068+
return new InvalidFileError(
1069+
'rewrap bad request; could indicate an invalid policy binding or a configuration error',
1070+
error
10231071
);
1072+
} else {
1073+
return new NetworkError('rewrap server error', error);
10241074
}
1075+
} else {
1076+
if (error.name === 'InvalidAccessError' || error.name === 'OperationError') {
1077+
return new DecryptError('unable to unwrap key from kas', error);
1078+
}
1079+
return new InvalidFileError(
1080+
`Unable to decrypt the response from KAS: [${error.name}: ${error.message}]`,
1081+
error
1082+
);
10251083
}
1026-
1027-
// Merge the unwrapped keys from each KAS
1028-
const reconstructedKey = keyMerge(rewrappedKeys);
1029-
const reconstructedKeyBinary = Binary.fromArrayBuffer(reconstructedKey);
1030-
1031-
return {
1032-
reconstructedKeyBinary,
1033-
metadata: responseMetadata,
1034-
};
10351084
}
10361085

10371086
async function decryptChunk(

0 commit comments

Comments
 (0)