Creating an encryption key to store secrets
Many applications interact with one or more external resources. These resources have their own security, and provide credentials to access them. There are many ways to store credentials, this article focuses on using AWS KMS to do that. For a review of all the different solutions, check out the Authress Academy article on Securing your secrets.
Not every service or application supports Bring Your Own Keys, so here we'll review the secure storage via a cloud provider's secret storage solution. Here's a simple way to store unlimited number of secrets, without making unnecessary API calls.
Setup​
- Create an AWS KMS key and alias. Make sure to give your application access to encrypt and decrypt data with the KMS key.
- Add the following encryption manager, and call
generateKeys()
to generate the cipher your service will use for all data. - At run time use the encryption manager, and read and write the encrypted data to your datastore.
Integration​
[Javascript Service] Encryption Manager for encrypting secrets
import { KMS } from 'aws-sdk';
import crypto from 'crypto';
const kmsClient = new KMS();
/**
* SETUP:
* 1. Create the KMS Key and specify it here:
*/
const KMS_KEY_ID = 'ALIAS_KEY_NAME_OR_KEY_ID';
/**
* 2. call generateKeys() once here, and then store that value in your source code here
*/
const encryptedPrivateKey = 'GENERATED_ENCRYPTED_PRIVATE_KEY';
const algorithm = 'aes-256-ctr';
class EncryptionManager {
constructor() {
this.privateKeyAsync = null;
}
async reEncrypt(encryptedSecret) {
const regionalKms = new KMS();
const secret = await regionalKms.decrypt({ CiphertextBlob: Buffer.from(encryptedSecret, 'base64') })
.promise().then(data => data.Plaintext.toString());
await this.encryptData(secret);
}
async generateKeys() {
const privateKeyResult = crypto.randomBytes(64).toString('base64').slice(0, 32);
const newEncryptedPrivateKey = await kmsClient.encrypt({ Plaintext: privateKeyResult, KeyId: KMS_KEY_ID })
.promise().then(r => r.CiphertextBlob.toString('base64'));
const secret = await kmsClient.decrypt({ CiphertextBlob: Buffer.from(newEncryptedPrivateKey, 'base64') })
.promise().then(data => data.Plaintext.toString());
if (secret !== privateKeyResult.toString('base64')) {
throw Error('EncryptedDataDoesNotMatch');
}
console.log(newEncryptedPrivateKey);
return newEncryptedPrivateKey;
}
async decryptData(fullEncryptionData) {
if (!this.privateKeyAsync) {
this.privateKeyAsync = kmsClient.decrypt({ CiphertextBlob: Buffer.from(encryptedPrivateKey, 'base64') })
.promise().then(keyData => keyData.Plaintext.toString());
}
try {
const privateKey = await this.privateKeyAsync;
const { data: encryptionData, iv: initializationVector } = JSON.parse(Buffer.from(fullEncryptionData, 'base64').toString());
const decipher = crypto.createDecipheriv(algorithm, privateKey, Buffer.from(initializationVector, 'base64'));
const data = Buffer.concat([decipher.update(Buffer.from(encryptionData, 'base64')), decipher.final()]);
return data.toString();
} catch (error) {
console.log({ title: 'Failed to decrypt data', level: 'ERROR', error, fullEncryptionData });
return null;
}
}
async encryptData(data) {
const privateKey = await kmsClient.decrypt({ CiphertextBlob: Buffer.from(encryptedPrivateKey, 'base64') })
.promise().then(keyData => keyData.Plaintext.toString());
const initializationVector = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, privateKey, initializationVector);
const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]);
const fullEncryptionData = Buffer.from(JSON.stringify({
data: encryptedData.toString('base64'), iv: initializationVector.toString('base64')
})).toString('base64');
const decryptedData = await this.decryptData(fullEncryptionData);
if (data !== decryptedData) {
throw Error('EncryptionNotSymmetric');
}
console.log('Encrypted Data: ', fullEncryptionData);
return fullEncryptionData;
}
}
export default EncryptionManager;