-
Notifications
You must be signed in to change notification settings - Fork 20
AWS-SDK Migration #2482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development/8.3
Are you sure you want to change the base?
AWS-SDK Migration #2482
Changes from 69 commits
4a04d9f
e977b6b
f2d1862
dfa82ee
6caa9ee
b0d7e19
30a9a72
b1ab841
2725f3e
5082db4
9050d1d
537edef
f42ae42
afa98e4
bbc5736
1860908
c1e4353
f22d161
80a5bc1
15a219d
3628f54
ccbf1cb
cb1406c
13566df
bc68e48
9b89515
a62b0c3
c1cb299
80c71ab
e001134
63b5ed7
c1dbc0b
ad89172
f2ebcbe
1f1282b
f2cc101
391b5f9
5888102
f9fa2b5
dab2b55
09a4d72
3bc9cda
eea48d8
ae16e73
b638159
e82f62b
beddd18
f44d462
41e613f
0267c8a
f2ed4e0
1836433
0f74c7d
9c44beb
e6b3c80
7b42293
448991a
8dfa1f1
89c858f
28666f9
e0289d8
5c2e854
ce54892
32b466b
fc47a03
5773f10
92209c8
ea0d556
6cd93bd
cd8c386
15155a9
ef70715
49ed63f
4e6ef09
5860483
c38a472
6d9293f
a219f29
86c9aa6
af97f95
454e8de
634e3d6
faa6574
cf48ed1
b6fc714
848973d
2679b84
4d1537c
11de95d
d6fb784
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,9 +24,7 @@ export default function constructChunkStringToSign( | |||||||||
| currentChunkHash = constants.emptyStringHash; | ||||||||||
| } else { | ||||||||||
| const hash = crypto.createHash('sha256'); | ||||||||||
| const temp = justDataChunk instanceof Buffer | ||||||||||
| ? hash.update(justDataChunk) | ||||||||||
| : hash.update(justDataChunk, 'binary'); | ||||||||||
| const temp = hash.update(justDataChunk); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If we are sure this is a buffer, we should update the function from justDataChunk?: Buffer | string,to justDataChunk?: Buffer, |
||||||||||
| currentChunkHash = temp.digest('hex'); | ||||||||||
| } | ||||||||||
| return `AWS4-HMAC-SHA256-PAYLOAD\n${timestamp}\n` + | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,16 @@ | |||||||||
| import { arsenalErrorAWSKMS } from '../utils'; | ||||||||||
| import { Agent as HttpAgent } from 'http'; | ||||||||||
| import { Agent as HttpsAgent } from 'https'; | ||||||||||
| import { KMS, AWSError } from 'aws-sdk'; | ||||||||||
| import { KMSClient, | ||||||||||
| CreateKeyCommand, | ||||||||||
| ScheduleKeyDeletionCommand, | ||||||||||
| GenerateDataKeyCommand, | ||||||||||
| EncryptCommand, | ||||||||||
| DecryptCommand, | ||||||||||
| ListKeysCommand, | ||||||||||
| NotFoundException, | ||||||||||
| KMSInvalidStateException } from '@aws-sdk/client-kms'; | ||||||||||
| const { NodeHttpHandler } = require('@smithy/node-http-handler'); | ||||||||||
| import * as werelogs from 'werelogs'; | ||||||||||
| import assert from 'assert'; | ||||||||||
| import { KMSInterface, KmsBackend, getKeyIdFromArn, KmsProtocol, KmsType, makeBackend } from '../KMSInterface'; | ||||||||||
|
|
@@ -45,42 +54,39 @@ interface ClientOptions { | |||||||||
|
|
||||||||||
| export default class Client implements KMSInterface { | ||||||||||
| private _supportsDefaultKeyPerAccount: boolean; | ||||||||||
| private client: KMS; | ||||||||||
| private client: KMSClient; | ||||||||||
| public readonly backend: KmsBackend<KmsType.external>; | ||||||||||
| public readonly noAwsArn?: boolean; | ||||||||||
|
|
||||||||||
| constructor(options: ClientOptions) { | ||||||||||
| this._supportsDefaultKeyPerAccount = true; | ||||||||||
| const { providerName, tls, ak, sk, region, endpoint, noAwsArn } = options.kmsAWS; | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lint : nit |
||||||||||
| const { providerName,tls, ak, sk, region, endpoint, noAwsArn } = options.kmsAWS; | ||||||||||
|
|
||||||||||
| const httpOptions = tls ? { | ||||||||||
| agent: new HttpsAgent({ | ||||||||||
| const requestHandler = new NodeHttpHandler({ | ||||||||||
| httpAgent: !tls ? new HttpAgent({ | ||||||||||
| keepAlive: true, | ||||||||||
| }) : undefined, | ||||||||||
| httpsAgent: tls ? new HttpsAgent({ | ||||||||||
| keepAlive: true, | ||||||||||
| rejectUnauthorized: tls.rejectUnauthorized, | ||||||||||
| ca: tls.ca, | ||||||||||
| cert: tls.cert, | ||||||||||
| minVersion: tls.minVersion, | ||||||||||
| maxVersion: tls.maxVersion, | ||||||||||
| key: tls.key, | ||||||||||
| }), | ||||||||||
| } : { | ||||||||||
| agent: new HttpAgent({ | ||||||||||
| keepAlive: true, | ||||||||||
| }), | ||||||||||
| }; | ||||||||||
| }) : undefined, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const credentials = (ak && sk) ? { | ||||||||||
| credentials: { | ||||||||||
| accessKeyId: ak, | ||||||||||
| secretAccessKey: sk, | ||||||||||
| }, | ||||||||||
| accessKeyId: ak, | ||||||||||
| secretAccessKey: sk, | ||||||||||
| } : undefined; | ||||||||||
|
|
||||||||||
| this.client = new KMS({ | ||||||||||
| this.client = new KMSClient({ | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know how we can re-introduce the keepAlive here? Is it from the lib directly? |
||||||||||
| region, | ||||||||||
| endpoint, | ||||||||||
| httpOptions, | ||||||||||
| ...credentials, | ||||||||||
| credentials, | ||||||||||
| requestHandler, | ||||||||||
| }); | ||||||||||
| this.backend = makeBackend(KmsType.external, KmsProtocol.aws_kms, providerName); | ||||||||||
| this.noAwsArn = noAwsArn; | ||||||||||
|
|
@@ -118,31 +124,22 @@ export default class Client implements KMSInterface { | |||||||||
|
|
||||||||||
| createMasterKey(logger: werelogs.Logger, cb: (err: Error | null, keyId?: string, keyArn?: string) => void): void { | ||||||||||
| logger.debug('AWS KMS: creating master encryption key'); | ||||||||||
| this.client.createKey({}, (err: AWSError, data) => { | ||||||||||
| if (err) { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to create master encryption key', { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| this.client.send(new CreateKeyCommand({})).then(data => { | ||||||||||
| const keyMetadata = data?.KeyMetadata; | ||||||||||
| logger.debug("AWS KMS: master encryption key created", { KeyMetadata: keyMetadata }); | ||||||||||
| let keyId: string; | ||||||||||
| if (this.noAwsArn) { | ||||||||||
| // Use KeyId when ARN is not wanted | ||||||||||
| // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain | ||||||||||
| keyId = keyMetadata?.KeyId!; | ||||||||||
| keyId = keyMetadata?.KeyId || ''; | ||||||||||
| } else { | ||||||||||
| // Prefer ARN, but fall back to KeyId if ARN is missing | ||||||||||
| // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain | ||||||||||
| keyId = keyMetadata?.Arn ?? keyMetadata?.KeyId!; | ||||||||||
| keyId = keyMetadata?.Arn ?? (keyMetadata?.KeyId || ''); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is important top keep the comment about double arn prefix just below. This one:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (sill applicable) |
||||||||||
| } | ||||||||||
| // May produce double arn prefix: scality arn + aws arn | ||||||||||
| // arn:scality:kms:external:aws_kms:custom:key/arn:aws:kms:region:accountId:key/cbd69d33-ba8e-4b56-8cfe | ||||||||||
| // If this is a problem, a config flag should be used to hide the scality arn when returning the KMS KeyId | ||||||||||
| // or aws arn when creating the KMS Key | ||||||||||
| const arn = `${this.backend.arnPrefix}${keyId}`; | ||||||||||
| cb(null, keyId, arn); | ||||||||||
| }).catch(err => { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to create master encryption key', { err }); | ||||||||||
| cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -163,30 +160,24 @@ export default class Client implements KMSInterface { | |||||||||
| KeyId: masterKeyId, | ||||||||||
| PendingWindowInDays: 7, | ||||||||||
| }; | ||||||||||
| this.client.scheduleKeyDeletion(params, (err: AWSError, data) => { | ||||||||||
| if (err) { | ||||||||||
| if (err.code === 'NotFoundException' || err.code === 'KMSInvalidStateException') { | ||||||||||
| // master key does not exist or is already pending deletion | ||||||||||
| logger.info('AWS KMS: key does not exist or is already pending deletion', | ||||||||||
| { masterKeyId, error: err }); | ||||||||||
| cb(null); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to delete master encryption key', { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| const command = new ScheduleKeyDeletionCommand(params); | ||||||||||
|
|
||||||||||
| this.client.send(command).then(data => { | ||||||||||
| if (data?.KeyState && data.KeyState !== 'PendingDeletion') { | ||||||||||
| const error = arsenalErrorAWSKMS('key is not in PendingDeletion state'); | ||||||||||
| logger.error('AWS KMS: failed to delete master encryption key', { err, data }); | ||||||||||
| logger.error('AWS KMS: failed to delete master encryption key', { data }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| cb(null); | ||||||||||
| }).catch(err => { | ||||||||||
| if (err instanceof NotFoundException || err instanceof KMSInvalidStateException) { | ||||||||||
| logger.info('AWS KMS: key does not exist or is already pending deletion', { masterKeyId, error: err }); | ||||||||||
| return cb(null); | ||||||||||
|
Comment on lines
+175
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we return no error we want at least to have a warning log I think?
Suggested change
|
||||||||||
| } | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to delete master encryption key', { err }); | ||||||||||
| return cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -199,31 +190,24 @@ export default class Client implements KMSInterface { | |||||||||
| const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn); | ||||||||||
| logger.debug("AWS KMS: generating data key", { cryptoScheme, masterKeyId, masterKeyIdOrArn }); | ||||||||||
| assert.strictEqual(cryptoScheme, 1); | ||||||||||
|
|
||||||||||
| const params = { | ||||||||||
| KeyId: masterKeyId, | ||||||||||
| KeySpec: 'AES_256', | ||||||||||
| KeySpec: 'AES_256' as const, | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| }; | ||||||||||
|
|
||||||||||
| this.client.generateDataKey(params, (err: AWSError, data) => { | ||||||||||
| if (err) { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to generate data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| this.client.send(new GenerateDataKeyCommand(params)).then(data => { | ||||||||||
| if (!data) { | ||||||||||
| const error = arsenalErrorAWSKMS("failed to generate data key: empty response"); | ||||||||||
| logger.error("AWS KMS: failed to generate data key: empty response"); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const isolatedPlaintext = this.safePlaintext(data.Plaintext as Buffer); | ||||||||||
|
|
||||||||||
| logger.debug('AWS KMS: data key generated'); | ||||||||||
| cb(null, isolatedPlaintext, Buffer.from(data.CiphertextBlob as Uint8Array)); | ||||||||||
| }).catch(err => { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to generate data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -244,14 +228,7 @@ export default class Client implements KMSInterface { | |||||||||
| Plaintext: plainTextDataKey, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| this.client.encrypt(params, (err: AWSError, data) => { | ||||||||||
| if (err) { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to cipher data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| this.client.send(new EncryptCommand(params)).then(data => { | ||||||||||
| if (!data) { | ||||||||||
| const error = arsenalErrorAWSKMS("failed to cipher data key: empty response"); | ||||||||||
| logger.error("AWS KMS: failed to cipher data key: empty response"); | ||||||||||
|
|
@@ -262,6 +239,10 @@ export default class Client implements KMSInterface { | |||||||||
| logger.debug('AWS KMS: data key ciphered'); | ||||||||||
| cb(null, Buffer.from(data.CiphertextBlob as Uint8Array)); | ||||||||||
| return; | ||||||||||
| }).catch(err => { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to cipher data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -281,14 +262,7 @@ export default class Client implements KMSInterface { | |||||||||
| CiphertextBlob: cipheredDataKey, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| this.client.decrypt(params, (err: AWSError, data) => { | ||||||||||
| if (err) { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to decipher data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| this.client.send(new DecryptCommand(params)).then(data => { | ||||||||||
| if (!data) { | ||||||||||
| const error = arsenalErrorAWSKMS("failed to decipher data key: empty response"); | ||||||||||
| logger.error("AWS KMS: failed to decipher data key: empty response"); | ||||||||||
|
|
@@ -300,41 +274,30 @@ export default class Client implements KMSInterface { | |||||||||
|
|
||||||||||
| logger.debug('AWS KMS: data key deciphered'); | ||||||||||
| cb(null, isolatedPlaintext); | ||||||||||
| }).catch(err => { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error('AWS KMS: failed to decipher data key', { err }); | ||||||||||
| cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * NOTE1: S3C-4833 KMS healthcheck is disabled in CloudServer | ||||||||||
| * NOTE2: The best approach for implementing the AWS KMS health check is still under consideration. | ||||||||||
| * In the meantime, this method is commented out to prevent potential issues related to costs or permissions. | ||||||||||
| * | ||||||||||
| * Reasons for commenting out: | ||||||||||
| * - frequent API calls can lead to increased expenses. | ||||||||||
| * - access key secret key used must have `kms:ListKeys` permissions | ||||||||||
| * | ||||||||||
| * Future potential actions: | ||||||||||
| * - implement caching mechanisms to reduce the number of API calls. | ||||||||||
| * - differentiate between error types (e.g., 500 vs. 403) for more effective error handling. | ||||||||||
| * Healthcheck function to verify KMS connectivity | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the old comment mentioning
Maybe you can check with Nicolas if the new implementation is OK, as now this is enabled? |
||||||||||
| */ | ||||||||||
| /* | ||||||||||
| healthcheck(logger: werelogs.Logger, cb: (err: Error | null) => void): void { | ||||||||||
| logger.debug("AWS KMS: performing healthcheck"); | ||||||||||
|
|
||||||||||
| const params = { | ||||||||||
| const command = new ListKeysCommand({ | ||||||||||
| Limit: 1, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| this.client.listKeys(params, (err, data) => { | ||||||||||
| if (err) { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error("AWS KMS healthcheck: failed to list keys", { err }); | ||||||||||
| cb(error); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| this.client.send(command).then(() => { | ||||||||||
| logger.debug("AWS KMS healthcheck: list keys succeeded"); | ||||||||||
| cb(null); | ||||||||||
| }).catch(err => { | ||||||||||
| const error = arsenalErrorAWSKMS(err); | ||||||||||
| logger.error("AWS KMS healthcheck: failed to list keys", { err }); | ||||||||||
| cb(error); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| */ | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| import type { AWSError } from 'aws-sdk'; | ||
| import { ArsenalError, errorInstances } from '../errors'; | ||
| import { allowedKmsErrors } from '../errors/kmsErrors'; | ||
|
|
||
|
|
@@ -32,20 +31,37 @@ export function arsenalErrorKMIP(err: string | Error) { | |
|
|
||
| const allowedKmsErrorCodes = Object.keys(allowedKmsErrors) as unknown as (keyof typeof allowedKmsErrors)[]; | ||
|
|
||
| // Local AWSError type for compatibility with v3 error handling | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure to understand the goal of the comment here, can you explain 🙏
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. before we used to rely on the old aws sdk library which is no longer the case with the migration so had to define the type in order to keep the compatibility with the error handling
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then maybe just a naming like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. keeping the same name avoids additionnal not needed changes as the PR is already extensive |
||
| export type AWSError = Error & { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure you can remove this and use the |
||
| name?: string; | ||
| $fault?: 'client' | 'server'; | ||
| $metadata?: { | ||
| httpStatusCode?: number; | ||
| requestId?: string; | ||
| attempts?: number; | ||
| totalRetryDelay?: number; | ||
| }; | ||
| $retryable?: { | ||
| throttling?: boolean; | ||
| }; | ||
| message?: string; | ||
| }; | ||
|
|
||
| function isAWSError(err: string | Error | AWSError): err is AWSError { | ||
| return (err as AWSError).code !== undefined | ||
| && (err as AWSError).retryable !== undefined; | ||
| return (err as AWSError).name !== undefined | ||
| && (err as AWSError).$metadata !== undefined; | ||
| } | ||
|
|
||
| export function arsenalErrorAWSKMS(err: string | Error | AWSError) { | ||
| if (isAWSError(err)) { | ||
| if (allowedKmsErrorCodes.includes(err.code as keyof typeof allowedKmsErrors)) { | ||
| return errorInstances[`KMS.${err.code}`].customizeDescription(err.message); | ||
| const errorCode = err.name; | ||
| if (allowedKmsErrorCodes.includes(errorCode as keyof typeof allowedKmsErrors)) { | ||
| return errorInstances[`KMS.${errorCode}`].customizeDescription(err.message); | ||
| } else { | ||
| // Encapsulate into a generic ArsenalError but keep the aws error code | ||
| return ArsenalError.unflatten({ | ||
| is_arsenal_error: true, | ||
| type: `KMS.${err.code}`, // aws s3 prefix kms errors with KMS. | ||
| type: `KMS.${errorCode}`, // aws s3 prefix kms errors with KMS. | ||
| code: 500, | ||
| description: `unexpected AWS_KMS error`, | ||
| stack: err.stack, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As
justDataChunkis either a string or a buffer w emust keep the'binary'option in the case it is a string