|
1 |
| -import axios from 'axios'; |
| 1 | +import axios, { AxiosError } from 'axios'; |
2 | 2 | import { unsigned } from './utils/buffer-crc32.js';
|
3 | 3 | import { exportSPKI, importX509 } from 'jose';
|
4 | 4 | import { DecoratedReadableStream } from './client/DecoratedReadableStream.js';
|
5 |
| -import { EntityObject } from '../../src/tdf/EntityObject.js'; |
| 5 | +import { EntityObject } from '../../src/tdf/index.js'; |
6 | 6 | import { pemToCryptoPublicKey, validateSecureUrl } from '../../src/utils.js';
|
7 | 7 | import { DecryptParams } from './client/builders.js';
|
8 | 8 | import { AssertionConfig, AssertionKey, AssertionVerificationKeys } from './assertions.js';
|
@@ -926,25 +926,10 @@ async function unwrapKey({
|
926 | 926 | }
|
927 | 927 | const { keyAccess } = manifest.encryptionInformation;
|
928 | 928 | const splitPotentials = splitLookupTableFactory(keyAccess, allowedKases);
|
929 |
| - |
930 |
| - let responseMetadata; |
931 | 929 | 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 |
| - } |
942 | 930 |
|
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) { |
946 | 932 | const url = `${keySplitInfo.url}/${isAppIdProvider ? '' : 'v2/'}rewrap`;
|
947 |
| - |
948 | 933 | const ephemeralEncryptionKeys = await cryptoService.cryptoToPemPair(
|
949 | 934 | await cryptoService.generateKeyPair()
|
950 | 935 | );
|
@@ -980,58 +965,122 @@ async function unwrapKey({
|
980 | 965 | };
|
981 | 966 | }
|
982 | 967 |
|
983 |
| - // Create a PoP token by signing the body so KAS knows we actually have a private key |
984 |
| - // Expires in 60 seconds |
985 | 968 | 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]; |
986 | 992 |
|
987 | 993 | 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 | + }) |
997 | 1004 | );
|
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 | + '' |
1011 | 1026 | );
|
1012 |
| - } else { |
1013 |
| - throw new NetworkError('rewrap server error', e); |
1014 | 1027 | }
|
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 |
1023 | 1071 | );
|
| 1072 | + } else { |
| 1073 | + return new NetworkError('rewrap server error', error); |
1024 | 1074 | }
|
| 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 | + ); |
1025 | 1083 | }
|
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 |
| - }; |
1035 | 1084 | }
|
1036 | 1085 |
|
1037 | 1086 | async function decryptChunk(
|
|
0 commit comments