Johan Byrén
Johan Byrén

Reputation: 918

Decrypt SES message from S3 with KMS, Node

I am not able to decrypt my messages I receive from my S3 bucket. They are encrypted with a KMS key. I use Node and Typescript.

I have tried some stuff but arrent able to make it work. Looking in to this links: https://github.com/gilt/node-s3-encryption-client/issues/3 and https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html

My code look like this now:

import * as AWS from 'aws-sdk';
import * as crypto from 'crypto';    

const s3 = new AWS.S3({ apiVersion: '2006-03-01', region: 'eu-west-1' });
const kms = new AWS.KMS({ apiVersion: '2014-11-01', region: 'eu-west-1' });

export const handler = LambdaUtils.lambdaHandler( 'onebox-email-service-SendMailToL4PFunction', async (event) => {
    const record = event.Records[0];

    const request = {
      Bucket: record.s3.bucket.name,
      Key: record.s3.object.key
    };

    const data = await s3.getObject(request).promise();
    const decryptData = await decryptSES(data);

    return decryptData;
  }
);

export const decryptSES = async (objectData) => {
  const metadata = objectData.Metadata || {};
  const kmsKeyBase64 = metadata['x-amz-key-v2'];
  const iv = metadata['x-amz-iv'];
  const tagLen = (metadata['x-amz-tag-len'] || 0) / 8;
  let algo = metadata['x-amz-cek-alg'];
  const encryptionContext = JSON.parse(metadata['x-amz-matdesc']);

  switch (algo) {
    case 'AES/GCM/NoPadding':
      algo = 'aes-256-gcm';
      break;
    case 'AES/CBC/PKCS5Padding':
      algo = 'aes-256-cbc';
      break;
    default:
      log.error({Message: 'Unsupported algorithm: ' + algo});
      return;
  }

 if (typeof (kmsKeyBase64) === 'undefined') {
   log.error('Error');
 }

 const kmsKeyBuffer = new Buffer(kmsKeyBase64, 'base64');
 const returnValue = await kms.decrypt({ CiphertextBlob: kmsKeyBuffer, EncryptionContext: encryptionContext }, (err, kmsData) => {
    if (err) {
      log.error({err});
      return null;
    } else {
      const data = objectData.Body.slice(0, -tagLen);
      const decipher = crypto.createDecipheriv( algo, kmsKeys.Plaintext[0], new Buffer(iv, 'base64'));
      if (tagLen !== 0) {
        const tag = objectData.Body.slice(-tagLen);
        decipher.setAuthTag(tag);
      }
        let dec = decipher.update(data, 'binary', 'utf8');
        dec += decipher.final('utf8');
        return dec;
      }
    }).promise();

    return returnValue;
  };

I get error in my lambda that look like this:

2019-02-05T17:06:19.015Z d9593ef7-635b-47b2-b881-ede2a396f88e Error: Invalid key length at new Decipheriv (crypto.js:267:16) at Object.createDecipheriv (crypto.js:627:10) at Response.l.decrypt (/var/task/email-from-s3.js:592:232696) at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:364:18) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:105:20) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14) at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10) at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12) at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10 at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9) at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:115:18) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14) at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)

What I can see in my logs I get the encrypted message from my s3 bucket, but then it is not possible to decrypt it.

Can someone please help me with this? I use Node and Typescript.

Upvotes: 1

Views: 1753

Answers (2)

Carlos M. Rivera
Carlos M. Rivera

Reputation: 58

After trying hard to get a grip on decrypting SES encrypted emails using the AWS SDK v3 for JavaScript, I hit a wall because there wasn't much clear info out there.

But thankfully, this thread helped a ton! Here's the code I ended up with after picking up tips from everyone here. Hopefully, it'll save others some headaches too:

**This code is based on the AWS SDK for JavaScript v3. **

Init S3Client and execute GetObjectCommand

    const client = new S3Client({
      region: process.env.AWS_REGION as string,
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
      },
    });
    
    const command = new GetObjectCommand({
      Bucket: bucket,
      Key: key,
    });

Get Metadata and Body using the S3Client

const { Metadata, Body } = await client.send(command);

Run decrypt functionand store the string result in decrypted variable. Now you can do whatever you need with the content

const decrypted = await decrypt(Metadata, Body as Readable);

decrypt.ts

import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms';
import { createDecipheriv } from 'crypto';
import { Readable } from 'stream';

export const decrypt = async (Metadata: Record<string, string>, Body: Readable): Promise<string> => {
  const body = await streamToString(Body);

  const Plaintext = await getKMS(Metadata);

  const { key, iv, tag, data } = getDecryptionParams(Metadata, body, Plaintext);

  //'aes-256-gcm' is required as algorithm in order to use setAuthTag method
  const decipher = createDecipheriv('aes-256-gcm', key, iv).setAuthTag(tag);

  const decryptedBuffer = decipher.update(data);
  const decrypted = decryptedBuffer.toString('utf8') + decipher.final('utf8');

  return decrypted;
};

const getDecryptionParams = (Metadata: Record<string, string>, body: Buffer, Plaintext: Uint8Array) => {
  const key = Buffer.from(Plaintext);
  const iv = Buffer.from(Metadata['x-amz-iv'], 'base64');
  const taglen = parseInt(Metadata['x-amz-tag-len'], 10) / 8;
  const tag = body.subarray(body.length - taglen);
  const data = body.subarray(0, body.length - taglen);

  return { key, iv, tag, data };
};

const getKMS = async (Metadata: Record<string, string>): Promise<Uint8Array> => {
  const client = new KMSClient({
    region: process.env.AWS_REGION as string,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
    },
  });

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

  if (!Plaintext) {
    throw new Error('No Plaintext found');
  }

  return Plaintext;
};

const streamToString = (stream: Readable): Promise<Buffer> =>
  new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];

    stream.on('data', (chunk) => chunks.push(chunk));

    stream.on('error', reject);

    stream.on('end', () => resolve(Buffer.concat(chunks)));
  });

Upvotes: 0

Johan Byr&#233;n
Johan Byr&#233;n

Reputation: 918

I was got some help from coworker and we could figuring it out. The problem was with the

const decipher = crypto.createDecipheriv( algo, kmsKeys.Plaintext[0], new Buffer(iv, 'base64'));

We needed to change the kms.Plaintext to kms.Plaintext as Buffer and it start working. I post my hole funktion here if someone needs it for later.

import * as AWS from 'aws-sdk';
import * as crypto from 'crypto';

const kms = new AWS.KMS({ apiVersion: '2014-11-01', region: 'eu-west-1' });

export const decryptS3Message = async (objectData) => {
  const metadata = objectData.Metadata || {};
  const kmsKeyBase64 = metadata['x-amz-key-v2'];
  const iv = metadata['x-amz-iv'];
  const tagLen = (metadata['x-amz-tag-len'] || 0) / 8;
  let algo = metadata['x-amz-cek-alg'];
  const encryptionContext = JSON.parse(metadata['x-amz-matdesc']);

  switch (algo) {
    case 'AES/GCM/NoPadding':
      algo = `aes-256-gcm`;
      break;
    case 'AES/CBC/PKCS5Padding':
      algo = `aes-256-cbc`;
      break;
    default:
      throw new ErrorUtils.NotFoundError('Unsupported algorithm: ' + algo);
  }

  if (typeof (kmsKeyBase64) === 'undefined') {
    return null;
  }

  const kmsKeyBuffer = Buffer.from(kmsKeyBase64, 'base64');

  const returnValue = await kms.decrypt({ CiphertextBlob: kmsKeyBuffer, EncryptionContext: encryptionContext }).promise()
    .then((res) => {
      const data = objectData.Body.slice(0, -tagLen);
      const decipher = crypto.createDecipheriv( algo, res.Plaintext as Buffer, Buffer.from(iv, 'base64'));
      if (tagLen !== 0) {
        const tag = objectData.Body.slice(-tagLen);
        decipher.setAuthTag(tag);
      }
      let dec = decipher.update(data, 'binary', 'utf8');
      dec += decipher.final('utf8');
      return dec;
    }).catch((err) => {
      throw new ErrorUtils.InternalServerError('Not able to decrypt message: ', err);
    });

  return returnValue;
};

Upvotes: 0

Related Questions