Skip to content

Commit 5390e5a

Browse files
Merge remote-tracking branch 'origin/w/8.1/improvement/ARSN-503-kms-err' into w/8.2/improvement/ARSN-503-kms-err
2 parents 77d5781 + f8cfa80 commit 5390e5a

File tree

5 files changed

+138
-18
lines changed

5 files changed

+138
-18
lines changed

lib/errors/arsenalErrors.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { allowedKmsErrors } from './kmsErrors';
2+
13
export type ErrorFormat = {
24
code: number,
35
description: string,
@@ -1072,13 +1074,13 @@ export const QuotaExceeded: ErrorFormat = {
10721074
};
10731075

10741076
// -------------------- KMS --------------------
1075-
// Most other KMS Exception can't be converted to from KMIP
1076-
const NotFoundException: ErrorFormat = {
1077-
code: 400, // Not 404 because it's the KMS (Encrypt/Decrypt) that fails, not the object API
1078-
description: 'The request was rejected because the specified entity or resource could not be found.'
1079-
};
1077+
type Entries<T> = {
1078+
[K in keyof T]: [K, T[K]];
1079+
}[keyof T];
10801080

10811081
/** need typescript@5.6 for string literal export
10821082
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html#support-for-arbitrary-module-identifiers
10831083
*/
1084-
module.exports['KMS.NotFoundException'] = NotFoundException;
1084+
for (const [kmsErr, err] of Object.entries(allowedKmsErrors) as Entries<typeof allowedKmsErrors>[]) {
1085+
module.exports[`KMS.${kmsErr}`] = err;
1086+
}

lib/errors/kmsErrors.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* List of AWS KMS errors that are specific to KMS and where the error
3+
* can be returned to the user as is with a specific status code.
4+
*
5+
* Avoid any error that might leak sensitive information such as hostnames
6+
* or IP addresses from network related errors.
7+
* KeyId is not considered sensitive.
8+
*
9+
* Reference: https://docs.aws.amazon.com/kms/latest/APIReference/CommonErrors.html
10+
* See specific actions like CreateKey, Encrypt, Decrypt, etc. for more details.
11+
*
12+
* Other errors not listed will return only the same error code but HTTP status 500,
13+
* and message will be generic and details will be in logs only.
14+
*/
15+
export const allowedKmsErrors = {
16+
AccessDeniedException: {
17+
code: 400,
18+
description: 'You do not have sufficient access to perform this action.',
19+
},
20+
AlreadyExistsException: {
21+
code: 400,
22+
description: 'The request was rejected because it attempted to create a resource that already exists.',
23+
},
24+
DisabledException: {
25+
code: 400,
26+
description: 'The request was rejected because the specified KMS key is not enabled.',
27+
},
28+
IncorrectKeyException: {
29+
code: 400,
30+
description: 'The request was rejected because the specified KMS key cannot decrypt the data',
31+
},
32+
InvalidAliasNameException: {
33+
code: 400,
34+
description: 'The request was rejected because the specified alias name is not valid.',
35+
},
36+
InvalidArnException: {
37+
code: 400,
38+
description: 'The request was rejected because a specified ARN, or an ARN in a key policy, is not valid.',
39+
},
40+
InvalidCiphertextException: {
41+
code: 400,
42+
description: 'The request was rejected because the specified ciphertext, or additional authenticated data, ' +
43+
'is corrupted, missing, or otherwise invalid.',
44+
},
45+
InvalidGrantTokenException: {
46+
code: 400,
47+
description: 'The request was rejected because the specified grant token is not valid.',
48+
},
49+
InvalidKeyUsageException: {
50+
code: 400,
51+
description: 'The KeyUsage or algorithm is incompatible with the API operation.',
52+
},
53+
KMSInternalException: {
54+
code: 500,
55+
description: 'The request was rejected because an internal exception occurred. The request can be retried.',
56+
},
57+
KMSInvalidStateException: {
58+
code: 400,
59+
description:
60+
'The request was rejected because the state of the specified resource is not valid for this request.',
61+
},
62+
KeyUnavailableException: {
63+
code: 500,
64+
description: 'The request was rejected because the specified KMS key was not available. ' +
65+
'You can retry the request.',
66+
},
67+
LimitExceededException: {
68+
code: 400,
69+
description: 'The request was rejected because a length constraint or quota was exceeded.',
70+
},
71+
MalformedPolicyDocumentException: {
72+
code: 400,
73+
description:
74+
'The request was rejected because the specified policy is not syntactically or semantically correct.',
75+
},
76+
/** Not 404 because it's the KMS (Encrypt/Decrypt) that fails, not the object API */
77+
NotFoundException: {
78+
code: 400,
79+
description: 'The request was rejected because the specified entity or resource could not be found.',
80+
},
81+
TagException: {
82+
code: 400,
83+
description: 'The request was rejected because one or more tags are not valid.',
84+
},
85+
UnsupportedOperationException: {
86+
code: 400,
87+
description: 'The request was rejected because a specified parameter is not supported or a specified ' +
88+
'resource is not valid for this operation.',
89+
},
90+
} as const;

lib/network/utils.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { errorInstances } from '../errors';
1+
import type { AWSError } from 'aws-sdk';
2+
import { ArsenalError, errorInstances } from '../errors';
3+
import { allowedKmsErrors } from '../errors/kmsErrors';
24

35
/**
46
* Normalize errors according to arsenal definitions with a custom prefix
@@ -28,6 +30,27 @@ export function arsenalErrorKMIP(err: string | Error) {
2830
return _normalizeArsenalError(err, 'KMIP:');
2931
}
3032

31-
export function arsenalErrorAWSKMS(err: string | Error) {
33+
const allowedKmsErrorCodes = Object.keys(allowedKmsErrors) as unknown as (keyof typeof allowedKmsErrors)[];
34+
35+
function isAWSError(err: string | Error | AWSError): err is AWSError {
36+
return (err as AWSError).code !== undefined
37+
&& (err as AWSError).retryable !== undefined;
38+
}
39+
40+
export function arsenalErrorAWSKMS(err: string | Error | AWSError) {
41+
if (isAWSError(err)) {
42+
if (allowedKmsErrorCodes.includes(err.code as keyof typeof allowedKmsErrors)) {
43+
return errorInstances[`KMS.${err.code}`].customizeDescription(err.message);
44+
} else {
45+
// Encapsulate into a generic ArsenalError but keep the aws error code
46+
return ArsenalError.unflatten({
47+
is_arsenal_error: true,
48+
type: `KMS.${err.code}`, // aws s3 prefix kms errors with KMS.
49+
code: 500,
50+
description: `unexpected AWS_KMS error`,
51+
stack: err.stack,
52+
});
53+
}
54+
}
3255
return _normalizeArsenalError(err, 'AWS_KMS:');
3356
}

lib/storage/data/DataWrapper.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,10 @@ class DataWrapper {
113113
stream.pipe(objectGetInfo.decipherStream);
114114
return cb(null, objectGetInfo.decipherStream);
115115
}
116-
// this should not happen anymore, could be deleted
116+
// There can still be unprepared decipherBundle
117+
// for objectCopy and objectPutCopyPart
118+
// Errors are caught and returned by the copy flow
117119
if (objectGetInfo.cipheredDataKey) {
118-
process.emitWarning(
119-
'DataWrapper.get should not use kms.createDecipherBundle',
120-
{
121-
code: 'S3C-9891',
122-
detail: 'Use a version of cloudserver that generate kms decipher stream',
123-
},
124-
);
125120
const serverSideEncryption = {
126121
cryptoScheme: objectGetInfo.cryptoScheme,
127122
masterKeyId: objectGetInfo.masterKeyId,
@@ -809,7 +804,7 @@ class DataWrapper {
809804
(err, cipherBundle) => {
810805
if (err) {
811806
log.debug('error getting cipherBundle');
812-
return cb(errors.InternalError);
807+
return cb(err);
813808
}
814809
return this._putForCopy(cipherBundle, stream, part,
815810
dataStoreContext, destBackendInfo, log, cb);
@@ -882,7 +877,7 @@ class DataWrapper {
882877
(err, cipherBundle) => {
883878
if (err) {
884879
log.debug('error getting cipherBundle', { error: err });
885-
return cb(errors.InternalError);
880+
return cb(err);
886881
}
887882
return this.put(cipherBundle, stream, numberPartSize,
888883
dataStoreContext, destBackendInfo, log,

tests/unit/errors.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ describe('Errors: ', () => {
9696
// don't use errorInstances if you need stacktrace
9797
expect(first.stack).toEqual(second.stack);
9898
});
99+
100+
it('should have KMS.NotFoundException', () => {
101+
const err = errorInstances['KMS.NotFoundException'];
102+
expect(err).toBeInstanceOf(ArsenalError);
103+
expect(err.is['KMS.NotFoundException']).toBeTruthy();
104+
expect(err.type).toEqual('KMS.NotFoundException');
105+
expect(err.code).toEqual(400);
106+
expect(err.description).toEqual(
107+
'The request was rejected because the specified entity or resource could not be found.');
108+
});
99109
});
100110

101111
describe('Backward compatibility flag', () => {

0 commit comments

Comments
 (0)