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