Cogito Interruptus Vulgaris
3 May 2018 | 2 min. (309 words)

Envelope encryption in Lambda functions with DynamoDB and KMS

Here's a quick code snippet on how to implement field level encryption of data stored in DynamoDB using per-record encryption keys and the AWS Key management store (KMS).

Before starting you'll want to create a KMS key that will just be used for this service and take note of the Key ID.

Also, I recommend enabling at-rest encryption of the DynamoDB table.

This example assumes a table structured similar to this:

{
  id: 'foobar',
  nonSensitiveStuff: 'will not be encrypted',
  sensitiveStuff: 'will be encrypted',
  otherStuff: 'will not be encrypted'
}

the id field is the primary key. As an enhancement, if your primary key is also sensitive you could instead use a hash of the ID instead of the actual value (although you'd lose the ability to search through the IDs).

const AWS = require('aws-sdk'); 
const crypto = require('crypto');

const ALGO = 'aes256';

class EnvelopeEncrypt {

  // Asuming input is object, and field we want to encrypt is sensitiveStuff
  // Also assuming that there is an 'id' field which is the primary key.
  // And also this assume UTF8 content...
  saveData(userInfo) { 
    const kms = new AWS.KMS();
    return kms.generateDataKey({ KeyId: 'YOUR_KMS_ENCRYPTION_KEY_ID', KeySpec: 'AES_256' }).promise()
      .then((dataKey) => {
        Object.assign(userInfo,{
          dataKey: dataKey.CiphertextBlob,
          sensitiveStuff: this.encrypt(dataKey.Plaintext, userInfo.sensitiveStuff)
        });
        const params = {
          TableName: 'YOUR_DYNAMO_DB_TABLE_NAME',
          Item: userInfo,
        };
        const dynamodb = new AWS.DynamoDB.DocumentClient();
        return dynamodb.put(params).promise();
      });
  }

  loadData(id) {
    const dynamodb = new AWS.DynamoDB.DocumentClient();
    const params = {
      TableName: 'YOUR_DYNAMO_DB_TABLE_NAME',
      Key: { id },
    };
    return dynamodb.get(params).promise()
      .then((record) => {
        if (record.Item !== undefined) {
          const kms = new AWS.KMS();
          return kms.decrypt({ CiphertextBlob: Buffer.from(record.Item.dataKey, 'base64') }).promise()
            .then((dataKey) => {
              Object.assign(record.Item, { sensitiveStuff: this.decrypt(dataKey.Plaintext.toString('base64'), record.Item.sensitiveStuff) });
              return record.Item;
            });
        }
        return undefined;
      });
  }

  encrypt(keyBase64, payload) {
    const cipher = crypto.createCipher(ALGO, Buffer.from(keyBase64, 'base64'));
    return (Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()])).toString('base64');
  }

  decrypt(keyBase64, payload) {
    const cipher = crypto.createDecipher(ALGO, Buffer.from(keyBase64, 'base64'));
    return (Buffer.concat([cipher.update(payload, 'base64'), cipher.final()])).toString('utf8');
  }
}

module.exports = EnvelopeEncrypt;
Reddit Twitter

Serverless content security policy

2018 Mark Steele