Skip to content

Commit 83c6ae9

Browse files
Merge remote-tracking branch 'origin/w/8.8/improvement/CLDSRV-670-kms-copy' into w/9.0/improvement/CLDSRV-670-kms-copy
2 parents eb5fa7b + 236fb84 commit 83c6ae9

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

lib/api/initiateMultipartUpload.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const { getObjectSSEConfiguration } = require('./apiUtils/bucket/bucketEncryptio
2323
const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders');
2424
const { setSSEHeaders } = require('./apiUtils/object/sseHeaders');
2525
const { updateEncryption } = require('./apiUtils/bucket/updateEncryption');
26+
const kms = require('../kms/wrapper');
2627

2728
/*
2829
Sample xml response:
@@ -359,6 +360,14 @@ function initiateMultipartUpload(authInfo, request, log, callback) {
359360
return next(null, corsHeaders, destinationBucket, objectSSEConfig);
360361
}
361362
),
363+
// If SSE configured, test kms key encryption access, but ignore cipher bundle
364+
(corsHeaders, destinationBucket, objectSSEConfig, next) => {
365+
if (objectSSEConfig) {
366+
return kms.createCipherBundle(objectSSEConfig, log,
367+
err => next(err, corsHeaders, destinationBucket, objectSSEConfig));
368+
}
369+
return next(null, corsHeaders, destinationBucket, objectSSEConfig);
370+
},
362371
],
363372
(error, corsHeaders, destinationBucket, objectSSEConfig) => {
364373
if (error) {

tests/functional/sse-kms-migration/arnPrefix.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,163 @@ describe('ensure MPU use good SSE', () => {
504504
);
505505
});
506506
});
507+
describe('KMS error', () => {
508+
const sseConfig = { algo: 'aws:kms', masterKeyId: true };
509+
const Bucket = 'bkt-kms-err';
510+
const Key = 'obj';
511+
const body = 'content';
512+
513+
let mpuEncrypted;
514+
let mpuPlaintext;
515+
516+
let masterKeyId;
517+
let masterKeyArn;
518+
519+
let expected;
520+
521+
const expectedKMIP = {
522+
code: 'KMS.NotFoundException',
523+
msg: (action, keyId) => new RegExp(`^KMS \\(KMIP\\) error for ${action} on ${keyId}\\..*`),
524+
};
525+
const expectedAWS = {
526+
code: 'KMS.KMSInvalidStateException',
527+
msg: (_, keyId) => new RegExp(`${keyId} is pending deletion\\.`),
528+
};
529+
/**
530+
* localkms container returns a different error message when the key is pending deletion
531+
* as we decrypt without passing the keyId, so we need to handle it separately
532+
*/
533+
const expectedLocalKms = {
534+
code: 'KMS.AccessDeniedException',
535+
msg: () => new RegExp(
536+
'The ciphertext refers to a customer master key that does not exist, ' +
537+
'does not exist in this region, or you are not allowed to access\\.'
538+
),
539+
};
540+
if (helpers.config.backends.kms === 'kmip') {
541+
expected = expectedKMIP;
542+
} else if (helpers.config.backends.kms === 'aws') {
543+
expected = expectedAWS;
544+
} else {
545+
throw new Error(`Unsupported KMS backend: ${helpers.config.backends.kms}`);
546+
}
547+
548+
function assertKmsError(action, keyId) {
549+
return err => {
550+
if (helpers.config.backends.kms === 'aws' && action === 'Decrypt') {
551+
assert.strictEqual(err.name, expectedLocalKms.code);
552+
assert.match(err.message, expectedLocalKms.msg(action, keyId));
553+
return true;
554+
}
555+
assert.strictEqual(err.name, expected.code);
556+
assert.match(err.message, expected.msg(action, keyId));
557+
return true;
558+
};
559+
}
560+
561+
before(async () => {
562+
void await helpers.s3.createBucket({ Bucket }).promise();
563+
564+
await helpers.s3.putObject({
565+
...helpers.putObjParams(Bucket, 'plaintext', {}, null),
566+
Body: body,
567+
}).promise();
568+
569+
mpuPlaintext = await helpers.s3.createMultipartUpload(
570+
helpers.putObjParams(Bucket, 'mpuPlaintext', {}, null)).promise();
571+
572+
({ masterKeyId, masterKeyArn } = await helpers.createKmsKey(log));
573+
574+
await helpers.putEncryptedObject(Bucket, Key, sseConfig, masterKeyArn, body);
575+
// ensure we can decrypt and read the object
576+
const obj = await helpers.s3.getObject({ Bucket, Key }).promise();
577+
assert.strictEqual(obj.Body.toString(), body);
578+
579+
mpuEncrypted = await helpers.s3.createMultipartUpload(
580+
helpers.putObjParams(Bucket, 'mpuEncrypted', sseConfig, masterKeyArn)).promise();
581+
582+
// make key unavailable
583+
void await helpers.destroyKmsKey(masterKeyArn, log);
584+
});
585+
586+
after(async () => {
587+
void await helpers.cleanup(Bucket);
588+
if (masterKeyArn) {
589+
try {
590+
void await helpers.destroyKmsKey(masterKeyArn, log);
591+
} catch (e) { void e; }
592+
[masterKeyArn, masterKeyId] = [null, null];
593+
}
594+
});
595+
596+
const testCases = [
597+
{
598+
action: 'putObject', kmsAction: 'Encrypt',
599+
fct: async ({ masterKeyArn }) =>
600+
helpers.putEncryptedObject(Bucket, 'fail', sseConfig, masterKeyArn, body),
601+
},
602+
{
603+
action: 'getObject', kmsAction: 'Decrypt',
604+
fct: async () => helpers.s3.getObject({ Bucket, Key }).promise(),
605+
},
606+
{
607+
action: 'copyObject', detail: ' when getting from source', kmsAction: 'Decrypt',
608+
fct: async () =>
609+
helpers.s3.copyObject({ Bucket, Key: 'copy', CopySource: `${Bucket}/${Key}` }).promise(),
610+
},
611+
{
612+
action: 'copyObject', detail: ' when putting to destination', kmsAction: 'Encrypt',
613+
fct: async ({ masterKeyArn }) => helpers.s3.copyObject({
614+
Bucket,
615+
Key: 'copyencrypted',
616+
CopySource: `${Bucket}/plaintext`,
617+
ServerSideEncryption: 'aws:kms',
618+
SSEKMSKeyId: masterKeyArn,
619+
}).promise(),
620+
},
621+
{
622+
action: 'createMPU', kmsAction: 'Encrypt',
623+
fct: async ({ masterKeyArn }) => helpers.s3.createMultipartUpload(
624+
helpers.putObjParams(Bucket, 'mpuKeyEncryptedFail', sseConfig, masterKeyArn)).promise(),
625+
},
626+
{
627+
action: 'mpu uploadPartCopy', detail: ' when getting from source', kmsAction: 'Decrypt',
628+
fct: async ({ mpuPlaintext }) => helpers.s3.uploadPartCopy({
629+
UploadId: mpuPlaintext.UploadId,
630+
Bucket,
631+
Key: 'mpuPlaintext',
632+
PartNumber: 1,
633+
CopySource: `${Bucket}/${Key}`,
634+
}).promise(),
635+
},
636+
{
637+
action: 'mpu uploadPart', detail: ' when putting to destination', kmsAction: 'Encrypt',
638+
fct: async ({ mpuEncrypted }) => helpers.s3.uploadPart({
639+
UploadId: mpuEncrypted.UploadId,
640+
Bucket,
641+
Key: 'mpuEncrypted',
642+
PartNumber: 1,
643+
Body: body,
644+
}).promise(),
645+
},
646+
{
647+
action: 'mpu uploadPartCopy', detail: ' when putting to destination', kmsAction: 'Encrypt',
648+
fct: async ({ mpuEncrypted }) => helpers.s3.uploadPartCopy({
649+
UploadId: mpuEncrypted.UploadId,
650+
Bucket,
651+
Key: 'mpuEncrypted',
652+
PartNumber: 1,
653+
CopySource: `${Bucket}/plaintext`,
654+
}).promise(),
655+
},
656+
];
657+
658+
testCases.forEach(({ action, kmsAction, fct, detail }) => {
659+
it(`${action} should fail with kms error${detail || ''}`, async () => {
660+
await assert.rejects(
661+
fct({ masterKeyArn, mpuEncrypted, mpuPlaintext }),
662+
assertKmsError(kmsAction, masterKeyId),
663+
);
664+
});
665+
});
666+
});

tests/functional/sse-kms-migration/helpers.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ async function createKmsKey(log) {
9393
});
9494
}
9595

96+
const destroyKmsKey = promisify(kms.destroyBucketKey);
97+
9698
async function cleanup(Bucket) {
9799
await bucketUtil.empty(Bucket);
98100
await s3.deleteBucket({ Bucket }).promise();
@@ -111,5 +113,6 @@ module.exports = {
111113
putEncryptedObject,
112114
getObjectMDSSE,
113115
createKmsKey,
116+
destroyKmsKey,
114117
cleanup,
115118
};

0 commit comments

Comments
 (0)