diff --git a/lib/api/apiUtils/bucket/updateEncryption.js b/lib/api/apiUtils/bucket/updateEncryption.js index 5db3152423..b8a01ced92 100644 --- a/lib/api/apiUtils/bucket/updateEncryption.js +++ b/lib/api/apiUtils/bucket/updateEncryption.js @@ -16,10 +16,9 @@ function updateBucketEncryption(bucket, log, cb) { const masterKey = sse.masterKeyId; const configuredKey = sse.configuredMasterKeyId; - // Note: if migration is from an external to an external, absence of arn is not enough - // a comparison of arn will be necessary but config validation blocks this for now - const updateMaster = masterKey && !isScalityKmsArn(masterKey); - const updateConfigured = configuredKey && !isScalityKmsArn(configuredKey); + // Replace keys of the KMS that SSE migration moves away from + const updateMaster = masterKey && kms.isMigrationSourceKey(masterKey); + const updateConfigured = configuredKey && kms.isMigrationSourceKey(configuredKey); if (!updateMaster && !updateConfigured) { return cb(null, bucket); @@ -87,8 +86,7 @@ function updateObjectEncryption(bucket, objMD, objectKey, log, keyArnPrefix, opt if (opts.skipObjectUpdate) { return cb(null, bucket, objMD); } - return metadata.putObjectMD(bucket.getName(), objectKey, objMD, params, - log, err => cb(err, bucket, objMD)); + return metadata.putObjectMD(bucket.getName(), objectKey, objMD, params, log, err => cb(err, bucket, objMD)); } /** diff --git a/lib/kms/wrapper.js b/lib/kms/wrapper.js index 772f6d2204..ec0c08112b 100644 --- a/lib/kms/wrapper.js +++ b/lib/kms/wrapper.js @@ -30,9 +30,7 @@ function getScalityKms() { scalityKMS = new ScalityKMS(config.kms); scalityKMSImpl = 'scalityKms'; } catch (error) { - logger.warn('scality kms unavailable. ' + - 'Using file kms backend unless mem specified.', - { error }); + logger.warn('scality kms unavailable. ' + 'Using file kms backend unless mem specified.', { error }); scalityKMS = file; scalityKMSImpl = 'fileKms'; } @@ -72,6 +70,10 @@ function getClient(kms) { return impl(); } +function getClientIdentifier(backend) { + return `${backend.type}:${backend.protocol}:${backend.provider}`; +} + /** * Note: non current instance from previous keys won't be healthchecked * `{ [`type:protocol:provider`]: clientDetails }` @@ -97,12 +99,10 @@ if (config.sseMigration) { previousBackend = makeBackend( config.sseMigration.previousKeyType, config.sseMigration.previousKeyProtocol, - config.sseMigration.previousKeyProvider + config.sseMigration.previousKeyProvider, ); availableBackends.push(previousBackend); - previousIdentifier = `${previousBackend.type - }:${previousBackend.protocol - }:${previousBackend.provider}`; + previousIdentifier = `${previousBackend.type}:${previousBackend.protocol}:${previousBackend.provider}`; // Pre instantiate previous backend as for now only internal backend (file) is supported // for future multiple external backend we should consider keeping open connection to @@ -152,13 +152,28 @@ function getClientForKey(key, log) { // Only pre instantiated previous KMS from sseMigration is supported now // Here we could instantiate other provider on the fly to manage multi providers - log.error('KMS key doesn\'t match any KMS instance', { key, detail, availableBackends }); - return { error: new errors.InvalidArgument - // eslint-disable-next-line new-cap - .customizeDescription(`KMS unknown provider for key ${key}`), + log.error("KMS key doesn't match any KMS instance", { key, detail, availableBackends }); + return { + error: new // eslint-disable-next-line new-cap + errors.InvalidArgument.customizeDescription(`KMS unknown provider for key ${key}`), }; } +/** + * Tell whether a stored key belongs to the KMS that SSE migration moves away from. + * A key ID without arn prefix belongs to the SSE-migration KMS. + * @param {string} storedKey - KeyId or KeyArn from metadata + * @param {object} [migrationBackend] - backend of the SSE-migration KMS, if configured + * @returns {boolean} true if the key belongs to the SSE-migration KMS + */ +function belongsToMigrationSource(storedKey, migrationBackend) { + if (!migrationBackend) { + return false; + } + const detail = extractDetailFromArn(storedKey); + return !detail.type || getClientIdentifier(detail) === getClientIdentifier(migrationBackend); +} + class KMS { /** Access to client for tests (to create a key without vault check and bucket) */ static get client() { @@ -170,20 +185,20 @@ class KMS { return client.backend.arnPrefix; } - /** - * Create a new bucket encryption key. - * - * This function is responsible for creating an encryption key for a bucket. - * If the client supports using a default master encryption key per account - * and one is configured, the key is managed at the account level by Vault. - * Otherwise, a bucket-level encryption key is created for legacy support. - * - * @param {BucketInfo} bucket - bucket info - * @param {object} log - logger object - * @param {function} cb - callback - * @returns {undefined} - * @callback called with (err, { masterKeyId: string, masterKeyArn: string, isAccountEncryptionEnabled: boolean }) - */ + /** + * Create a new bucket encryption key. + * + * This function is responsible for creating an encryption key for a bucket. + * If the client supports using a default master encryption key per account + * and one is configured, the key is managed at the account level by Vault. + * Otherwise, a bucket-level encryption key is created for legacy support. + * + * @param {BucketInfo} bucket - bucket info + * @param {object} log - logger object + * @param {function} cb - callback + * @returns {undefined} + * @callback called with (err, { masterKeyId: string, masterKeyArn: string, isAccountEncryptionEnabled: boolean }) + */ static createBucketKey(bucket, log, cb) { // always use current client for create log.debug('creating a new bucket key'); @@ -194,14 +209,19 @@ class KMS { if (client.supportsDefaultKeyPerAccount && config.defaultEncryptionKeyPerAccount) { return vault.getOrCreateEncryptionKeyId(bucket.getOwner(), log, (err, data) => { if (err) { - log.debug('error retrieving or creating the default encryption key at the account level from vault', - { implName, error: err }); + log.debug( + 'error retrieving or creating the default encryption key at the account level from vault', + { implName, error: err }, + ); return cb(err); } const { encryptionKeyId, action } = data; - log.trace('default encryption key retrieved or created at the account level from vault', - { implName, encryptionKeyId, action }); + log.trace('default encryption key retrieved or created at the account level from vault', { + implName, + encryptionKeyId, + action, + }); return cb(null, { // vault only return arn masterKeyId: encryptionKeyId, @@ -221,15 +241,15 @@ class KMS { }); } - /** - * - * @param {BucketInfo} bucket - bucket info - * @param {object} sseConfig - SSE configuration - * @param {object} log - logger object - * @param {function} cb - callback - * @returns {undefined} - * @callback called with (err, serverSideEncryptionInfo: object) - */ + /** + * + * @param {BucketInfo} bucket - bucket info + * @param {object} sseConfig - SSE configuration + * @param {object} log - logger object + * @param {function} cb - callback + * @returns {undefined} + * @callback called with (err, serverSideEncryptionInfo: object) + */ static bucketLevelEncryption(bucket, sseConfig, log, cb) { /* The purpose of bucket level encryption is so that the client does not @@ -258,8 +278,8 @@ class KMS { } serverSideEncryptionInfo.configuredMasterKeyId = configuredMasterKeyId; } else { - serverSideEncryptionInfo.configuredMasterKeyId = - `${client.backend.arnPrefix}${configuredMasterKeyId}`; + // eslint-disable-next-line max-len + serverSideEncryptionInfo.configuredMasterKeyId = `${client.backend.arnPrefix}${configuredMasterKeyId}`; } return process.nextTick(() => cb(null, serverSideEncryptionInfo)); @@ -280,9 +300,9 @@ class KMS { return cb(null, serverSideEncryptionInfo); }); } - /* - * no encryption - */ + /* + * no encryption + */ return cb(null, null); } @@ -311,26 +331,34 @@ class KMS { }); } - /** - * createCipherBundle - * @param {object} serverSideEncryptionInfo - info for encryption - * @param {number} serverSideEncryptionInfo.cryptoScheme - - * cryptoScheme used - * @param {string} serverSideEncryptionInfo.algorithm - - * algorithm to use - * @param {string} serverSideEncryptionInfo.masterKeyId - - * key to get master key - * @param {boolean} serverSideEncryptionInfo.mandatory - - * true for mandatory encryption - * @param {object} log - logger object - * @param {function} cb - cb from external call - * @param {object} [opts] - additional options - * @param {boolean} [opts.previousOk] - allow usage of previous KMS (for ongoing MPU not migrated) - * @returns {undefined} - * @callback called with (err, cipherBundle) - */ - static createCipherBundle(serverSideEncryptionInfo, - log, cb, opts) { + /** + * Tell whether a stored key belongs to the KMS that SSE migration moves away from + * @param {string} storedKey - KeyId or KeyArn from metadata + * @returns {boolean} true if a key of the default KMS must replace it + */ + static isMigrationSourceKey(storedKey) { + return belongsToMigrationSource(storedKey, previousBackend); + } + + /** + * createCipherBundle + * @param {object} serverSideEncryptionInfo - info for encryption + * @param {number} serverSideEncryptionInfo.cryptoScheme - + * cryptoScheme used + * @param {string} serverSideEncryptionInfo.algorithm - + * algorithm to use + * @param {string} serverSideEncryptionInfo.masterKeyId - + * key to get master key + * @param {boolean} serverSideEncryptionInfo.mandatory - + * true for mandatory encryption + * @param {object} log - logger object + * @param {function} cb - cb from external call + * @param {object} [opts] - additional options + * @param {boolean} [opts.previousOk] - allow usage of previous KMS (for ongoing MPU not migrated) + * @returns {undefined} + * @callback called with (err, cipherBundle) + */ + static createCipherBundle(serverSideEncryptionInfo, log, cb, opts) { const { algorithm, configuredMasterKeyId, masterKeyId: bucketMasterKeyId } = serverSideEncryptionInfo; let masterKeyId = bucketMasterKeyId; @@ -345,14 +373,18 @@ class KMS { if (error) { return cb(error); } - if (previousIdentifier - && clientIdentifier === previousIdentifier - && clientIdentifier !== currentIdentifier - && (opts && !opts.previousOk) + if ( + previousIdentifier && + clientIdentifier === previousIdentifier && + clientIdentifier !== currentIdentifier && + opts && + !opts.previousOk ) { - return cb(errors.InvalidArgument - .customizeDescription( - 'KMS cannot use previous provider to encrypt new objects if a new provider is configured')); + return cb( + errors.InvalidArgument.customizeDescription( + 'KMS cannot use previous provider to encrypt new objects if a new provider is configured', + ), + ); } const cipherBundle = { @@ -363,9 +395,10 @@ class KMS { cipher: null, }; - return async.waterfall([ - function generateDataKey(next) { - /* There are 2 ways of generating a datakey : + return async.waterfall( + [ + function generateDataKey(next) { + /* There are 2 ways of generating a datakey : - using the generateDataKey of the KMS backend if it exists (currently only implemented for the AWS KMS backend). This is the preferred solution since a dedicated KMS should offer a better @@ -374,92 +407,96 @@ class KMS { encrypt the datakey. This method is used when the KMS backend doesn't provide the generateDataKey method. */ - let res; - if (client.generateDataKey) { - log.debug('creating a data key using the KMS'); - res = client.generateDataKey(cipherBundle.cryptoScheme, - key, - log, (err, plainTextDataKey, cipheredDataKey) => { - if (err) { - log.debug('error generating a new data key from KMS', - { implName, error: err }); - return next(err); - } - log.trace('data key generated by the kms'); - return next(null, plainTextDataKey, cipheredDataKey); - }); - } else { - log.debug('creating a new data key'); - const plainTextDataKey = Common.createDataKey(); - - log.debug('ciphering the data key'); - res = client.cipherDataKey(cipherBundle.cryptoScheme, - key, - plainTextDataKey, log, (err, cipheredDataKey) => { - if (err) { - log.debug('error encrypting the data key using KMS', - { implName, error: err }); - return next(err); - } - log.trace('data key ciphered by the kms'); - return next(null, plainTextDataKey, cipheredDataKey); - }); - } - return res; - }, - function createCipher(plainTextDataKey, cipheredDataKey, next) { - log.debug('creating a cipher'); - cipherBundle.cipheredDataKey = - cipheredDataKey.toString('base64'); - return Common.createCipher(cipherBundle.cryptoScheme, - plainTextDataKey, 0, log, (err, cipher) => { + let res; + if (client.generateDataKey) { + log.debug('creating a data key using the KMS'); + res = client.generateDataKey( + cipherBundle.cryptoScheme, + key, + log, + (err, plainTextDataKey, cipheredDataKey) => { + if (err) { + log.debug('error generating a new data key from KMS', { implName, error: err }); + return next(err); + } + log.trace('data key generated by the kms'); + return next(null, plainTextDataKey, cipheredDataKey); + }, + ); + } else { + log.debug('creating a new data key'); + const plainTextDataKey = Common.createDataKey(); + + log.debug('ciphering the data key'); + res = client.cipherDataKey( + cipherBundle.cryptoScheme, + key, + plainTextDataKey, + log, + (err, cipheredDataKey) => { + if (err) { + log.debug('error encrypting the data key using KMS', { implName, error: err }); + return next(err); + } + log.trace('data key ciphered by the kms'); + return next(null, plainTextDataKey, cipheredDataKey); + }, + ); + } + return res; + }, + function createCipher(plainTextDataKey, cipheredDataKey, next) { + log.debug('creating a cipher'); + cipherBundle.cipheredDataKey = cipheredDataKey.toString('base64'); + return Common.createCipher(cipherBundle.cryptoScheme, plainTextDataKey, 0, log, (err, cipher) => { plainTextDataKey.fill(0); if (err) { - log.debug('error from kms', - { implName, error: err }); + log.debug('error from kms', { implName, error: err }); return next(err); } log.trace('cipher created by the kms'); return next(null, cipher); }); + }, + function finishCipherBundle(cipher, next) { + cipherBundle.cipher = cipher; + return next(null, cipherBundle); + }, + ], + (err, cipherBundle) => { + if (err) { + log.error('error processing cipher bundle', { implName, error: err }); + } + return cb(err, cipherBundle); }, - function finishCipherBundle(cipher, next) { - cipherBundle.cipher = cipher; - return next(null, cipherBundle); - }, - ], (err, cipherBundle) => { - if (err) { - log.error('error processing cipher bundle', - { implName, error: err }); - } - return cb(err, cipherBundle); - }); + ); } - /** - * createDecipherBundle - * @param {object} serverSideEncryptionInfo - info for decryption - * @param {number} serverSideEncryptionInfo.cryptoScheme - - * cryptoScheme used - * @param {string} serverSideEncryptionInfo.algorithm - - * algorithm to use - * @param {string} serverSideEncryptionInfo.masterKeyId - - * key to get master key - * @param {boolean} serverSideEncryptionInfo.mandatory - - * true for mandatory encryption - * @param {buffer} serverSideEncryptionInfo.cipheredDataKey - - * ciphered data key - * @param {number} offset - offset for decryption - * @param {object} log - logger object - * @param {function} cb - cb from external call - * @returns {undefined} - * @callback called with (err, decipherBundle) - */ - static createDecipherBundle(serverSideEncryptionInfo, offset, - log, cb) { - if (!serverSideEncryptionInfo.masterKeyId || + /** + * createDecipherBundle + * @param {object} serverSideEncryptionInfo - info for decryption + * @param {number} serverSideEncryptionInfo.cryptoScheme - + * cryptoScheme used + * @param {string} serverSideEncryptionInfo.algorithm - + * algorithm to use + * @param {string} serverSideEncryptionInfo.masterKeyId - + * key to get master key + * @param {boolean} serverSideEncryptionInfo.mandatory - + * true for mandatory encryption + * @param {buffer} serverSideEncryptionInfo.cipheredDataKey - + * ciphered data key + * @param {number} offset - offset for decryption + * @param {object} log - logger object + * @param {function} cb - cb from external call + * @returns {undefined} + * @callback called with (err, decipherBundle) + */ + static createDecipherBundle(serverSideEncryptionInfo, offset, log, cb) { + if ( + !serverSideEncryptionInfo.masterKeyId || !serverSideEncryptionInfo.cipheredDataKey || - !serverSideEncryptionInfo.cryptoScheme) { + !serverSideEncryptionInfo.cryptoScheme + ) { log.error('Invalid cryptographic information', { implName }); return cb(errors.InternalError); } @@ -469,55 +506,61 @@ class KMS { }; // shadowing global client for key - implName already used can't be shadowed here - const { error, client, implName: _impl, key } = getClientForKey( - serverSideEncryptionInfo.masterKeyId, log); + const { error, client, implName: _impl, key } = getClientForKey(serverSideEncryptionInfo.masterKeyId, log); if (error) { return cb(error); } - return async.waterfall([ - function decipherDataKey(next) { - return client.decipherDataKey( - decipherBundle.cryptoScheme, - key, - serverSideEncryptionInfo.cipheredDataKey, - log, (err, plainTextDataKey) => { - log.debug('deciphering a data key'); - if (err) { - log.debug('error from kms', - { implName: _impl, error: err }); - return next(err); - } - log.trace('data key deciphered by the kms'); - return next(null, plainTextDataKey); - }); - }, - function createDecipher(plainTextDataKey, next) { - log.debug('creating a decipher'); - return Common.createDecipher(decipherBundle.cryptoScheme, - plainTextDataKey, offset, log, (err, decipher) => { - plainTextDataKey.fill(0); - if (err) { - log.debug('error from kms', - { implName: _impl, error: err }); - return next(err); - } - log.trace('decipher created by the kms'); - return next(null, decipher); - }); - }, - function finishDecipherBundle(decipher, next) { - decipherBundle.decipher = decipher; - return next(null, decipherBundle); + return async.waterfall( + [ + function decipherDataKey(next) { + return client.decipherDataKey( + decipherBundle.cryptoScheme, + key, + serverSideEncryptionInfo.cipheredDataKey, + log, + (err, plainTextDataKey) => { + log.debug('deciphering a data key'); + if (err) { + log.debug('error from kms', { implName: _impl, error: err }); + return next(err); + } + log.trace('data key deciphered by the kms'); + return next(null, plainTextDataKey); + }, + ); + }, + function createDecipher(plainTextDataKey, next) { + log.debug('creating a decipher'); + return Common.createDecipher( + decipherBundle.cryptoScheme, + plainTextDataKey, + offset, + log, + (err, decipher) => { + plainTextDataKey.fill(0); + if (err) { + log.debug('error from kms', { implName: _impl, error: err }); + return next(err); + } + log.trace('decipher created by the kms'); + return next(null, decipher); + }, + ); + }, + function finishDecipherBundle(decipher, next) { + decipherBundle.decipher = decipher; + return next(null, decipherBundle); + }, + ], + (err, decipherBundle) => { + if (err) { + log.error('error processing decipher bundle', { implName: _impl, error: err }); + return cb(err); + } + return cb(err, decipherBundle); }, - ], (err, decipherBundle) => { - if (err) { - log.error('error processing decipher bundle', - { implName: _impl, error: err }); - return cb(err); - } - return cb(err, decipherBundle); - }); + ); } static checkHealth(log, cb) { @@ -571,3 +614,4 @@ class KMS { } module.exports = KMS; +module.exports.belongsToMigrationSource = belongsToMigrationSource; diff --git a/package.json b/package.json index 102c646a3d..da6eb57051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.3.13-3", + "version": "9.3.13-4", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": { @@ -134,7 +134,7 @@ "ft_kmip_cluster": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 300000 load.js --exit", "ft_sse_cleanup": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 cleanup.js --exit", "ft_sse_before_migration": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 cleanup.js beforeMigration.js --exit", - "ft_sse_migration": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 migration.js --exit", + "ft_sse_migration": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 migration.js internalBucketKeys.js --exit", "ft_sse_arn": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 cleanup.js arnPrefix.js --exit", "lint": "eslint $(git ls-files '*.js')", "lint_md": "mdlint $(git ls-files '*.md')", diff --git a/tests/functional/sse-kms-migration/internalBucketKeys.js b/tests/functional/sse-kms-migration/internalBucketKeys.js new file mode 100644 index 0000000000..554f682738 --- /dev/null +++ b/tests/functional/sse-kms-migration/internalBucketKeys.js @@ -0,0 +1,106 @@ +const assert = require('assert'); +const kms = require('../../../lib/kms/wrapper'); +const filekms = require('../../../lib/kms/file/backend'); +const { DummyRequestLogger } = require('../../unit/helpers'); +const helpers = require('./helpers'); + +const log = new DummyRequestLogger(); +const arnPrefixReg = new RegExp(`^${kms.arnPrefix}`); + +function createInternalKeyArn() { + return new Promise((resolve, reject) => + filekms.createBucketKey('bucket', log, (err, keyId, keyArn) => (err ? reject(err) : resolve(keyArn))), + ); +} + +async function getBucketSSEMD(Bucket) { + const bucketMD = await helpers.MD.getBucket(Bucket, log); + return bucketMD.getServerSideEncryption(); +} + +describe('SSE KMS migration of internal bucket keys with ARN prefix', () => { + before(async () => helpers.MD.setup()); + + [ + { + name: 'created by S3C', + Bucket: 'enc-bkt-internal-arn-master-key', + keyField: 'masterKeyId', + setBucketKey: async (Bucket, keyArn) => { + const bucketMD = await helpers.MD.getBucket(Bucket, log); + bucketMD.setServerSideEncryption({ + cryptoScheme: 1, + algorithm: 'aws:kms', + mandatory: true, + masterKeyId: keyArn, + }); + await helpers.MD.updateBucket(Bucket, bucketMD, log); + }, + }, + { + name: 'set by the user', + Bucket: 'enc-bkt-internal-arn-configured-key', + keyField: 'configuredMasterKeyId', + setBucketKey: (Bucket, keyArn) => + helpers.s3.putBucketEncryption({ + Bucket, + ServerSideEncryptionConfiguration: helpers.hydrateSSEConfig({ + algo: 'aws:kms', + masterKeyId: keyArn, + }), + }), + }, + ].forEach(({ name, Bucket, keyField, setBucketKey }) => + describe(`bucket key ${name}`, () => { + const oldObj = { Key: 'old-obj', Body: 'BODY(old-obj)' }; + const newObj = { Key: 'new-obj', Body: 'BODY(new-obj)' }; + let internalKeyArn; + let replacedSSE; + + before(async () => { + internalKeyArn = await createInternalKeyArn(); + await helpers.s3.createBucket({ Bucket }); + // Put before the bucket has a key: an object request would replace it + await helpers.putEncryptedObject( + Bucket, + oldObj.Key, + { algo: 'aws:kms', masterKeyId: true }, + internalKeyArn, + oldObj.Body, + ); + await setBucketKey(Bucket, internalKeyArn); + const sse = await getBucketSSEMD(Bucket); + assert.strictEqual(sse[keyField], internalKeyArn); + }); + + after(async () => helpers.cleanup(Bucket)); + + it('should replace the bucket key by a key of the default KMS on HeadObject', async () => { + await helpers.s3.headObject({ Bucket, Key: oldObj.Key }); + replacedSSE = await getBucketSSEMD(Bucket); + assert.match(replacedSSE[keyField], arnPrefixReg); + }); + + it('should keep the internal key of an existing object and decrypt it', async () => { + const sseMD = await helpers.getObjectMDSSE(Bucket, oldObj.Key); + assert.strictEqual(sseMD.SSEKMSKeyId, internalKeyArn); + const get = await helpers.s3.getObject({ Bucket, Key: oldObj.Key }); + assert.strictEqual(get.Body, oldObj.Body); + }); + + it('should encrypt a new object with the replacing key', async () => { + await helpers.s3.putObject({ Bucket, ...newObj }); + const sseMD = await helpers.getObjectMDSSE(Bucket, newObj.Key); + assert.match(sseMD.SSEKMSKeyId, arnPrefixReg); + assert.strictEqual(sseMD.SSEKMSKeyId, replacedSSE[keyField]); + const get = await helpers.s3.getObject({ Bucket, Key: newObj.Key }); + assert.strictEqual(get.Body, newObj.Body); + }); + + it('should keep the replacing key on later requests', async () => { + await helpers.s3.headObject({ Bucket, Key: oldObj.Key }); + assert.deepStrictEqual(await getBucketSSEMD(Bucket), replacedSSE); + }); + }), + ); +}); diff --git a/tests/unit/encryption/belongsToMigrationSource.js b/tests/unit/encryption/belongsToMigrationSource.js new file mode 100644 index 0000000000..4052f86b0f --- /dev/null +++ b/tests/unit/encryption/belongsToMigrationSource.js @@ -0,0 +1,47 @@ +const assert = require('assert'); +const { KmsType, KmsProtocol, makeBackend } = require('arsenal/build/lib/network/KMSInterface'); +const { belongsToMigrationSource } = require('../../../lib/kms/wrapper'); + +const internal = makeBackend(KmsType.internal, KmsProtocol.file, 'scality'); +const kmip = makeBackend(KmsType.external, KmsProtocol.kmip, 'thales'); +const awsKms = makeBackend(KmsType.external, KmsProtocol.aws_kms, 'localaws'); +const keyId = '891a72205529b8a7531362d59d485f2fd182bb17cc7b07b2613fb01a2f966d56'; + +describe('belongsToMigrationSource', () => { + [ + { + desc: 'a key ID without ARN prefix', + storedKey: keyId, + migrationBackend: internal, + expected: true, + }, + { + desc: 'a key ARN of the SSE-migration KMS', + storedKey: `${internal.arnPrefix}${keyId}`, + migrationBackend: internal, + expected: true, + }, + { + desc: 'a key ARN of a KMIP KMS', + storedKey: `${kmip.arnPrefix}931`, + migrationBackend: internal, + expected: false, + }, + { + desc: 'a key ARN of an AWS KMS', + storedKey: `${awsKms.arnPrefix}36d31b52-c5a7-4e1d-9f0b-2a8c3e6d7f41`, + migrationBackend: internal, + expected: false, + }, + { + desc: 'a key ARN of the internal KMS without SSE migration', + storedKey: `${internal.arnPrefix}${keyId}`, + migrationBackend: undefined, + expected: false, + }, + ].forEach(({ desc, storedKey, migrationBackend, expected }) => + it(`should return ${expected} for ${desc}`, () => { + assert.strictEqual(belongsToMigrationSource(storedKey, migrationBackend), expected); + }), + ); +});