Skip to content

Commit

Permalink
fix: keySplitInfo Promise.any Promise.all (#379)
Browse files Browse the repository at this point in the history
* Fix EntityObject import path and add unwrapKey tests

Updated the import path for EntityObject to ensure correct module resolution. Added a comprehensive set of tests for the unwrapKey function to validate error handling and successful key unwrapping scenarios.

* Refactor unwrapKey to parallelize KAS attempts

Parallelize the KAS key rewrap attempts within unwrapKey for improved efficiency. All potential KAS sources are now processed concurrently, and the method uses the first successful result, enhancing performance and robustness. Errors are collected and the most relevant one is thrown if all attempts fail.

* Delete unwrapKey test suite

Removed the entire unwrapKey.spec.ts test suite as it is no longer necessary. Minor formatting adjustments were made in the tdf.ts file.

* Add detailed error handling tests for encryption-decryption

Introduced comprehensive error handling tests for various HTTP status codes in encryption-decryption processes, including 400, 401, 403, and 500 HTTP errors. Adjusted the mock server to validate error conditions based on custom headers and handle network failures.

* 🤖 🎨 Autoformat

* x-test-response-message

* Refactor KAS source error handling with Promise.any

Simplify the KAS handling logic by using `Promise.any` to stop at the first successful response rather than waiting for all promises to complete. This ensures more efficient error handling and improves overall performance by immediately processing the first successful KAS response, while adequately handling AggregateErrors for failed attempts.

* Update TypeScript config to ES2021 standards

Updated the TypeScript configuration in both "lib" and "cli" projects to use ES2021 language features and modules. This change improves consistency and supports newer JavaScript features.

* 🤖 🎨 Autoformat

* lastError

* reformat

* AND OR

---------

Co-authored-by: pflynn-virtru <[email protected]>
  • Loading branch information
pflynn-virtru and pflynn-virtru authored Nov 13, 2024
1 parent 496f07c commit c6cdbef
Show file tree
Hide file tree
Showing 5 changed files with 429 additions and 76 deletions.
2 changes: 1 addition & 1 deletion cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "../lib/tsconfig.json",
"compilerOptions": {
"lib": ["es2020", "ES2022.Error"],
"lib": ["es2021", "ES2022.Error"],
"module": "Node16",
"outDir": "dist"
},
Expand Down
171 changes: 110 additions & 61 deletions lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { unsigned } from './utils/buffer-crc32.js';
import { exportSPKI, importX509 } from 'jose';
import { DecoratedReadableStream } from './client/DecoratedReadableStream.js';
import { EntityObject } from '../../src/tdf/EntityObject.js';
import { EntityObject } from '../../src/tdf/index.js';
import { pemToCryptoPublicKey, validateSecureUrl } from '../../src/utils.js';
import { DecryptParams } from './client/builders.js';
import { AssertionConfig, AssertionKey, AssertionVerificationKeys } from './assertions.js';
Expand Down Expand Up @@ -926,25 +926,10 @@ async function unwrapKey({
}
const { keyAccess } = manifest.encryptionInformation;
const splitPotentials = splitLookupTableFactory(keyAccess, allowedKases);

let responseMetadata;
const isAppIdProvider = authProvider && isAppIdProviderCheck(authProvider);
// Get key access information to know the KAS URLS
const rewrappedKeys: Uint8Array[] = [];

for (const [splitId, potentials] of Object.entries(splitPotentials)) {
if (!potentials || !Object.keys(potentials).length) {
throw new UnsafeUrlError(
`Unreconstructable key - no valid KAS found for split ${JSON.stringify(splitId)}`,
''
);
}

// If we have multiple ways of getting a value, try the 'best' way
// or maybe retry across all potential ways? Currently, just tries them all
const [keySplitInfo] = Object.values(potentials);
async function tryKasRewrap(keySplitInfo: KeyAccessObject) {
const url = `${keySplitInfo.url}/${isAppIdProvider ? '' : 'v2/'}rewrap`;

const ephemeralEncryptionKeys = await cryptoService.cryptoToPemPair(
await cryptoService.generateKeyPair()
);
Expand Down Expand Up @@ -980,58 +965,122 @@ async function unwrapKey({
};
}

// Create a PoP token by signing the body so KAS knows we actually have a private key
// Expires in 60 seconds
const httpReq = await authProvider.withCreds(buildRequest('POST', url, requestBody));
const {
data: { entityWrappedKey, metadata },
} = await axios.post(httpReq.url, httpReq.body, { headers: httpReq.headers });

const key = Binary.fromString(base64.decode(entityWrappedKey));
const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
key,
ephemeralEncryptionKeys.privateKey
);

return {
key: new Uint8Array(decryptedKeyBinary.asByteArray()),
metadata,
};
}

// Get unique split IDs to determine if we have an OR or AND condition
const splitIds = new Set(Object.keys(splitPotentials));

// If we have only one split ID, it's an OR condition
if (splitIds.size === 1) {
const [splitId] = splitIds;
const potentials = splitPotentials[splitId];

try {
// The response from KAS on a rewrap
const {
data: { entityWrappedKey, metadata },
} = await axios.post(httpReq.url, httpReq.body, { headers: httpReq.headers });
responseMetadata = metadata;
const key = Binary.fromString(base64.decode(entityWrappedKey));
const decryptedKeyBinary = await cryptoService.decryptWithPrivateKey(
key,
ephemeralEncryptionKeys.privateKey
// OR condition: Try all KAS servers for this split, take first success
const result = await Promise.any(
Object.values(potentials).map(async (keySplitInfo) => {
try {
return await tryKasRewrap(keySplitInfo);
} catch (e) {
// Rethrow with more context
throw handleRewrapError(e as Error | AxiosError);
}
})
);
rewrappedKeys.push(new Uint8Array(decryptedKeyBinary.asByteArray()));
} catch (e) {
if (e.response) {
if (e.response.status >= 500) {
throw new ServiceError('rewrap failure', e);
} else if (e.response.status === 403) {
throw new PermissionDeniedError('rewrap failure', e);
} else if (e.response.status === 401) {
throw new UnauthenticatedError('rewrap auth failure', e);
} else if (e.response.status === 400) {
throw new InvalidFileError(
'rewrap bad request; could indicate an invalid policy binding or a configuration error',
e

const reconstructedKey = keyMerge([result.key]);
return {
reconstructedKeyBinary: Binary.fromArrayBuffer(reconstructedKey),
metadata: result.metadata,
};
} catch (error) {
if (error instanceof AggregateError) {
// All KAS servers failed
throw error.errors[0]; // Throw the first error since we've already wrapped them
}
throw error;
}
} else {
// AND condition: We need successful results from all different splits
const splitResults = await Promise.all(
Object.entries(splitPotentials).map(async ([splitId, potentials]) => {
if (!potentials || !Object.keys(potentials).length) {
throw new UnsafeUrlError(
`Unreconstructable key - no valid KAS found for split ${JSON.stringify(splitId)}`,
''
);
} else {
throw new NetworkError('rewrap server error', e);
}
} else if (e.request) {
throw new NetworkError('rewrap request failure', e);
} else if (e.name == 'InvalidAccessError' || e.name == 'OperationError') {
throw new DecryptError('unable to unwrap key from kas', e);
}
throw new InvalidFileError(
`Unable to decrypt the response from KAS: [${e.name}: ${e.message}], response: [${e?.response?.body}]`,
e

try {
// For each split, try all potential KAS servers until one succeeds
return await Promise.any(
Object.values(potentials).map(async (keySplitInfo) => {
try {
return await tryKasRewrap(keySplitInfo);
} catch (e) {
throw handleRewrapError(e as Error | AxiosError);
}
})
);
} catch (error) {
if (error instanceof AggregateError) {
// All KAS servers for this split failed
throw error.errors[0]; // Throw the first error since we've already wrapped them
}
throw error;
}
})
);

// Merge all the split keys
const reconstructedKey = keyMerge(splitResults.map((r) => r.key));
return {
reconstructedKeyBinary: Binary.fromArrayBuffer(reconstructedKey),
metadata: splitResults[0].metadata, // Use metadata from first split
};
}
}

function handleRewrapError(error: Error | AxiosError) {
if (axios.isAxiosError(error)) {
if (error.response?.status && error.response?.status >= 500) {
return new ServiceError('rewrap failure', error);
} else if (error.response?.status === 403) {
return new PermissionDeniedError('rewrap failure', error);
} else if (error.response?.status === 401) {
return new UnauthenticatedError('rewrap auth failure', error);
} else if (error.response?.status === 400) {
return new InvalidFileError(
'rewrap bad request; could indicate an invalid policy binding or a configuration error',
error
);
} else {
return new NetworkError('rewrap server error', error);
}
} else {
if (error.name === 'InvalidAccessError' || error.name === 'OperationError') {
return new DecryptError('unable to unwrap key from kas', error);
}
return new InvalidFileError(
`Unable to decrypt the response from KAS: [${error.name}: ${error.message}]`,
error
);
}

// Merge the unwrapped keys from each KAS
const reconstructedKey = keyMerge(rewrappedKeys);
const reconstructedKeyBinary = Binary.fromArrayBuffer(reconstructedKey);

return {
reconstructedKeyBinary,
metadata: responseMetadata,
};
}

async function decryptChunk(
Expand Down
Loading

0 comments on commit c6cdbef

Please sign in to comment.