From Idea to Reality: The Power of Serverless for Modern Applications
Why is encryption necessary?
With the increased number of data breaches in recent times, data security and privacy have become the most important facet for businesses. Organizations today rely heavily on the power of encryption to make data transfer and storage as confidential and secure as possible.
For data in transit, TLS/SSL provides a secure session between a client and a server and prevents any man-in-the-middle attacks but encrypting data at rest remains much of a concern. While mobile applications could implement end-to-end encryption, a similar approach is not feasible for web applications. In such scenarios, the concept of envelope encryption comes to the rescue and provides a better alternative to secure the data.
What is envelope encryption?
Envelope encryption is the method of encrypting data with a data encryption key (DEK) and then encrypting the DEK with a fully manageable root key. Multiple cloud platforms provide key management services, AWS Key Management Service being one of them. This provides an additional layer of data security from data breaches and unauthorized access by combining the strength of multiple encryption algorithms to protect the data.
AWS Key Management Service
AWS Key Management Service (KMS) is a fully managed service that assists in the creation and management of cryptographic keys and provides a centralized control across a wide range of AWS services and in user’s applications. The maximum size of data that could be encrypted or decrypted using KMS CMK is 4KB. To encrypt/decrypt data more than that KMS uses the concept of envelope encryption.
Below mentioned are the flows required to implement envelope encryption using AWS KMS.
Encrypting a file
For encrypting a file, firstly we need to request KMS to generate a data key. KMS would return an object with an encrypted and plain text version of the data key. Post that, the plain text data key could be used to encrypt the file using some encryption algorithm.
We should destroy the plain text data key from the memory and save the encrypted data key which would be referenced to decrypt the file later.
Decrypting a file
For decrypting a file, firstly we need to get the plain text data key that was used to encrypt it. Since our CMK in KMS generated the plain text and encrypted data key pair, it can decrypt the encrypted data key and return the plain text key.
The saved encrypted data key would be sent to KMS to be decrypted using our CMK. Post the retrieval of the plain text data key we can use it to decrypt the encrypted file using the decryption algorithm.
Demo Application
Let’s create a demo app in Nodejs to encrypt and decrypt a file using envelope encryption with AWS Key Management Service before storing it in S3 and after retrieving it.
Prerequisites:
- Create a S3 bucket
- Create a CMK in AWS KMS
- Create a DynamoDB table
1. Create an encrypt and decrypt function using a crypto library of nodejs that accepts a plain text data key and file buffer to be encrypted and decrypted.
const encrypt = (buffer, key) => {
const cipher = crypto.createCipheriv(algorithm, key, iv)
const result = Buffer.concat([cipher.update(buffer), cipher.final()])
return result
}
const decrypt = (cipherBuffer, key) => {
const decipher = crypto.createDecipheriv(algorithm, key, iv)
const result = Buffer.concat([
decipher.update(cipherBuffer),
decipher.final(),
])
return result
}
2. In the upload endpoint
// generating a data key from AWS KMS
params = {
KeyId: process.env.KMS_ARN, // ARN or KeyId of the CMK
KeySpec: "AES_256", // type of data key to be returned
}
//returns plain and cipher text form of the data key
const dataKey = await kms.generateDataKey(params).promise()
// encrypting the file using the plain text version of the data key
const encryptedBuffer = encrypt(file.data, dataKey.Plaintext)
//making the plain text data key null
for (let i = 0; i < dataKey.Plaintext.length; i++) {
dataKey.Plaintext[i] = null
}
generateDataKey() of KMS returns plain text and encrypted versions of a data key generated by our KMS CMK. The plain text data key with the file buffer is passed to the encrypt() which returns the encrypted buffer.
// storing the encrypted file in S3
params = {
Bucket: process.env.BUCKET_NAME, // name of the bucket
Body: encryptedBuffer,
Key: file.name,
}
await s3.putObject(params).promise()
// creating a record in dynamodb for file and encrypted data key
params = {
TableName: process.env.TABLE_NAME, // name of the table
Item: {
id: uuidv4(),
encryptedDataKey: dataKey.CiphertextBlob,
fileName: file.name,
},
}
await dynamodb.put(params).promise()
The encrypted buffer is stored in S3 and the meta-data of the file and the encrypted data key is stored in DynamoDB.
3. In the download endpoint
// Fetching the encrypted data key from dynamodb table
params = {
TableName: process.env.TABLE_NAME, // table name of dynamodb
Key: {
id: fileId,
},
}
const record = await dynamodb.get(params).promise()
//Fetching the file from S3
params = {
Bucket: process.env.BUCKET_NAME,
Key: record.Item.fileName,
}
const s3Res = await s3.getObject(params).promise()
The encrypted data key and the file meta-data are fetched from the DynamoDB post which the encrypted file is fetched from S3.
// decrypting the encrypted dataKey
params = {
CiphertextBlob: record.Item.encryptedDataKey, //encrypted data key
KeyId: process.env.KMS_ARN, // ARN or KeyId of the CMK
}
const decryptedDataKey = await kms.decrypt(params).promise()
// decrypting the encrypted file from S3
const decryptedBuffer = decrypt(s3Res.Body, decryptedDataKey.Plaintext)
decrypt() of KMS decrypts the encrypted data key with the CMK and returns the plain text version which is passed to our decrypt() with the encrypted file buffer. Our decrypt function decrypts the file and is sent back to the client.
Full source code: https://github.com/antstackio/aws-kms-envelope-encryption.