0

I am trying to decrypt a KMS encrypted AWS SES message stored in S3.

Everything works fine (getting the object and metadata from s3, decrypting the key with KMS) until I try to decrypt the content with the decrypted Key:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms';
import { createDecipheriv } from 'crypto';

const s3 = new S3Client( {
    // ...credentials
} );

const kms = new KMSClient( {
    // ...credentials
} );

const getFromS3AndDecrypt = ( Bucket, Key ) => {

    // GetObject command
    const command = new GetObjectCommand( {
        Bucket,
        Key,
    } );
    
    // Get the object from S3
    const { Metadata, Body } = await s3.send( command );

    // Convert to Buffer
    const body = await streamToBuffer( Body );

    // Get KMS Metadata
    var {
        [ 'x-amz-key-v2' ]: kmsKeyBase64,
        [ 'x-amz-iv' ]: iv,
        [ 'x-amz-tag-len' ]: tagLenBits = 0,
        [ 'x-amz-cek-alg' ]: algo,
        [ 'x-amz-matdesc' ]: matdesc,
    } = Metadata;

    // Convert tagLenBits to Bytes
    const tagLen = tagLenBits / 8;

    // Get encryption context
    const encryptionContext = JSON.parse( matdesc );

    // Get algorithm
    switch ( algo ) {

        case 'AES/GCM/NoPadding':
            algo = `aes-256-gcm`;
            break;

        case 'AES/CBC/PKCS5Padding':
            algo = `aes-256-cbc`;
            break;

        default:
            throw new Error( `Unsupported algorithm ${ algo }` );

    }

    // Convert kmsKey to Buffer
    const kmsKeyBuffer = Buffer.from( kmsKeyBase64, 'base64' );

    // DecryptCommand
    const kmsCommand = new DecryptCommand( {
        CiphertextBlob: kmsKeyBuffer,
        EncryptionContext: encryptionContext,
    } );

    // Decrypt the key with KMS
    const { Plaintext } = await kmsClient.send( kmsCommand );

    // Create decipher with key and iv
    const decipher = createDecipheriv( algo, Buffer.from( Plaintext ), Buffer.from( iv ), {
        authTagLength: 16,
    } );

    // Body without authTag
    const data = body.slice( 0, - tagLen );

    if  ( tagLen !== 0 ) {
        
        // authTag
        const tag = body.slice( - tagLen );

        // Set authTag
        decipher.setAuthTag( tag );

    }

    // Decrypt data
    var decrypted = decipher.update( data, 'binary', 'utf8' );
    
    decrypted += decipher.final( 'utf8' );

    return decrypted;

};

// Turn @aws-sdk/client-s3 stream response into buffer @see https://github.com/aws/aws-sdk-js-v3/issues/1877#issuecomment-755387549
const streamToBuffer = ( stream ) => new Promise( ( resolve, reject ) => {
        
    const chunks = [];
    
    stream.on( 'data', ( chunk ) => chunks.push( chunk ) );
    
    stream.on( 'error', reject );
    
    stream.on( 'end', () => resolve( Buffer.concat( chunks ) ) );
    
} );

When calling decipher.final I get a Unsupported state or unable to authenticate data error.

This seems to be either because the authTag used doesn't match the one which was used when encrypting the data or because the input and output encoding doesn't match.

Snowball
  • 1,402
  • 2
  • 17
  • 31

1 Answers1

0

Both the iv and Plaintext key are already base64 encoded and thus the encoding needs to be passed to the Buffer:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms';
import { createDecipheriv } from 'crypto';

const s3 = new S3Client( {
    // ...credentials
} );

const kms = new KMSClient( {
    // ...credentials
} );

const getFromS3AndDecrypt = ( Bucket, Key ) => {

    const { Metadata, Body } = await s3.send( new GetObjectCommand( {
        Bucket,
        Key,
    } ) );

    const body = await streamToString( Body );

    const { Plaintext } = await kms.send( new DecryptCommand( {
        CiphertextBlob: Buffer.from( Metadata[ 'x-amz-key-v2' ], 'base64' ),
        EncryptionContext: JSON.parse( Metadata[ 'x-amz-matdesc' ] ),
    } ) );

    const key = Buffer.from( Plaintext, 'base64' );

    const iv = Buffer.from( Metadata[ 'x-amz-iv' ], 'base64' );

    const tag = body.slice( -16 );

    const data = body.slice( 0, -16 );

    const decipher = createDecipheriv( 'aes-256-gcm', key, iv );

    decipher.setAuthTag( tag );

    var decrypted = decipher.update( data, 'binary', 'utf8' );

    decrypted += decipher.final( 'utf8' );

    return decrypted;

};

// Turn @aws-sdk/client-s3 stream response into buffer @see https://github.com/aws/aws-sdk-js-v3/issues/1877#issuecomment-755387549
const streamToString = ( stream ) => new Promise( ( resolve, reject ) => {
        
    const chunks = [];
    
    stream.on( 'data', ( chunk ) => chunks.push( chunk ) );
    
    stream.on( 'error', reject );
    
    stream.on( 'end', () => resolve( Buffer.concat( chunks ) ) );
    
} );
Snowball
  • 1,402
  • 2
  • 17
  • 31