Skip to main content

Creating an encryption key to store secrets

Many applications interact with one or more external resources. These resources have their own security, and store the credentials for access in a secure way is critical to the safety of the data they can access.

In the case that security store is advanced enough to understand Authress client authentication, it's easy to follow the guide to create client credentials.

However, not every application and cloud provider has hooks to verify the caller permissions and identity. In these other cases, it's possible to use a cloud provider's secret storage solution. Here's a simple way to store unlimited number of secrets, without making unnecessary API calls.

Setup

  1. In the case of using AWS KMS, create a KMS key and alias. Make sure to give your application access to encrypt and decrypt data with the KMS key.
  2. Add the following encryption manager, and call generateKeys() to generate the cipher your service will use for all data.
  3. At run time use the encryption manager, and read and write the encrypted data to your datastore.

Integration

encryptionManager.js
const { KMS } = require('aws-sdk');
const crypto = require('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;
}
}

module.exports = new EncryptionManager();