@@ -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+ } ) ;
0 commit comments