KMS and Envelope Encryption Explained

Juan Carlos Garzon
iCapital Network Technology Group
7 min readJun 17, 2020

What is a KMS (Key Management System)?

Behind every bit of encrypted information, there is a key that unlocks that information. Due to the complexity of modern encryption, it becomes necessary to create and maintain a system that allows you to create, manage, assign, decommission keys, as well as perform the base encryption and decryption functions. This kind of system is known as a KMS or a Key Management System. Having a KMS is so crucial that even Amazon offers its own KMS system to the public.

Limitations of Amazon KMS

While Amazon KMS is a highly considered and useful solution. Its flaw lies in how your keys are stored by regions. So if you were to create encryption keys in the US-East region, it would mean that if there were a disaster that caused an outage to Amazon services on the East coast of the US, these keys would be unavailable to you, and prevent you from accessing your data during a disaster. This is a worst-case scenario, and if your don’t have a need for a robust disaster recovery plan, then you could use Amazon KMS without much worry.

However, let's practice extreme caution and work around Amazon’s KMS region problem. We’ll do this by creating our own KMS Service and manage our own data security strategy.

Basic Encryption

The base of any KMS, is the ability to encrypt and decrypt data. Node has a built-in crypto library. This gives us access to an encryption API, allowing us to choose and implement from many different cryptographic algorithms from OpenSSL. If you want to see a full list of supported cryptographic algorithms, run the following command in your terminalopenssl list -cipher-algorithms.

Choosing a cryptographic algorithm is a topic worthy of discussion on its own. However, for our purposes, let’s keep it simple and just use AES-128-CBC in our examples.

const crypto = require("crypto");
const ALGORITHM = "aes-128-cbc";
const key = "plainTextMasterKey";const encrypt = function(str) {
let cipher = crypto.createCipher(ALGORITHM, key);
let ciphertext = cipher.update(str, "utf8", "hex");
return ciphertext + cipher.final("hex");
};
const decrypt = function(encrypted_str) {
let cipher = crypto.createDecipher(ALGORITHM, key);
let text = cipher.update(encrypted_str, "hex", "utf8");
return text + cipher.final("utf8");
};
module.exports = {
basicEncrypt: encrypt,
basicDecrypt: decrypt
};

The above uses Encryptor/Decryptor, the basis of which is pretty simple, we need a key and we use that get to set up the ciphers for the createCipher and createDecipher methods. Then we simply encode and encrypt the string, and do it reverse when we want to decode and decrypt the encryption result.

You can take some time and play in the sandbox, and see how this works in action.
Source Code: https://codesandbox.io/s/basic-encryption-3sccu?file=/src/basicCryptor.js

Envelope Encryption

Now we have a basic understanding of how to encrypt and decrypt data. For a functional KMS system, we need to add the ability to support multiple keys. But this presents a new problem, how does our KMS know which key was used to encrypt which file?

Standard Encryption Workflow

This where envelope encryption comes in. In our previous example, we used a key to lock the data, like in the diagram above. However, in envelope encryption, we take it one step further, we use a separate key and append it to the encrypted file. In essence, we have 2 separate keys, and to avoid confusion, we’ll call the encryption key the crypt key, and the other the data key.

Envelope Encryption combines the Data Key with the encrypted file

Keep in mind that the data key is not the same as the crypt key, you will not be able to use it to unlock encrypted information no matter who has it. It's simply an identifier that lets you know which key was used to lock the data. So let’s take a look at an example.

const crypto = require("crypto");
const ALGORITHM = "aes-128-cbc";
const DATAKEYS = {
dataKey1: { cryptKey: "plainTextMasterKey1" },
dataKey2: { cryptKey: "plainTextMasterKey2" },
dataKey3: { cryptKey: "plainTextMasterKey3" }
};
const DATAKEY_LENGTH = 8;const getRandomKey = function() {
const ranNum = Math.floor((Math.random() * 10) % 3);
return Object.keys(DATAKEYS)[ranNum];
};
const encrypt = function(str) {
const dataKey = getRandomKey();
const cryptKey = DATAKEYS[dataKey].cryptKey;
const cipher = crypto.createCipher(ALGORITHM, cryptKey);
const ciphertext = cipher.update(str, "utf8", "hex"); return dataKey + ciphertext + cipher.final("hex");
};
const decrypt = function(enveloped_str) {
const dataKey = enveloped_str.slice(0, DATAKEY_LENGTH);
const cryptKey = DATAKEYS[dataKey].cryptKey;
const cryptedText = enveloped_str.substring(DATAKEY_LENGTH);
const cipher = crypto.createDecipher(ALGORITHM, cryptKey);
const text = cipher.update(cryptedText, "hex", "utf8");
return text + cipher.final("utf8");};module.exports = {
encrypt: encrypt,
decrypt: decrypt
};

In this implementation of envelope encryption, a hash map is used to map the crypt keys, to the data keys. Then I randomly choose one data key and its associated crypt key, to encrypt the data and then append the data key with the resulting encryption.

Notice that no matter which data key is used, the decrypt method here will always be able to decode the information. We ensure this by keeping our data keys the same length. This way the decrypt method knows which part of the data is the data key and which part is the encrypted data. Also, keep in mind you don’t want to use anything readable like dataKey1 as the data key. You should randomly generate your data key, so it appears as if it's part of the encrypted data, and then you can append it to the start or end of the file.

Take some time and play in the sandbox, and see how this works in action.

Source Code: https://codesandbox.io/s/envelope-encryption-6bezb?file=/src/cryptor.js

Putting it all together KMS and the Keybox

Envelope encryption alone does not make a key management system. So far our examples have flaws such as our crypt keys being stored as plaintext, and the lack of flexibility as to which encryption key we want to apply.

Real-World Keybox

To add these new features let’s introduce the key box. It is our centralized location for managing keys.

Our previous examples had our keys stored in a hash, where our crypt keys were stored as plain text. This is a bad practice because it leaves our keys vulnerable to anyone who has access to the source code. Ideally, you want to keep the circle of people who have access to these keys small.

We can do this by keeping our data keys stored in a database, along with our encrypted crypt keys. Our crypt keys should never be plaintext in our database. In keeping with the principle of keeping our crypt keys as secret as possible, we also want to protect them from anyone who has database access.

We can leverage the Amazon KMS to just encrypt and decrypt our crypt keys. Since we only need to rely on it for the initialization of our keys, and not the active encrypting of our data, the region problem doesn't hamper us. This is because our crypt keys will already have been decrypted and loaded into memory once our server starts up.

const guids = require("./guids");
const Sequelize = require("sequelize");
// Always password protect your databases, this is just an example
var sequelize = new Sequelize("database", "", "", {
host: "0.0.0.0",
dialect: "sqlite",
pool: {
max: 5,
min: 0,
idle: 10000
},
storage: "src/db/database.sqlite"
});
const KEYS = {};
var KMS_DATABASE;
function initKeys() {
console.log("++++ Initializing Keys ++++");
let promise = new Promise((resolve, reject) => {
sequelize.authenticate().then(function(err) {
console.log("Connection established.");
KMS_DATABASE = sequelize.define("kms_dev",{
/* Define your table columns here */
});
KMS_DATABASE.findAll().then(function(kms) {
// finds all entries in the kms_devs table
kms.forEach(itm => {
// Here I'm using the plain text for this example
// But here is where you would decrypt encrypted_crypt_key
// By using Amazon KMS, or any other decryption strategy.
const decryptedKey = itm.encrypted_crypt_key;
KEYS[itm.client_id] = {
[itm.data_key]: {
binary: decryptedKey,
disabled: itm.disabled
}
};
});
// KEYS have been initalized and the app can now start
resolve();
});
}).catch(function(err) {
console.log("Unable to connect to database: ", err);
});
});
return promise;
}
function getClientDataKeys(client_id) {
return KEYS[client_id];
}
function validClient(client_id) {
return getClientDataKeys(client_id) !== undefined;
}
function validKey(client_id, key_id) {
const client = getClientDataKeys(client_id);
return client[key_id] !== undefined;
}
function getKey(client_id) {
const clientKeys = getClientDataKeys(client_id);
const dataKeys = Object.keys(clientKeys);
const enabledKey = dataKeys.find(key => {
return !clientKeys[key].disabled;
});
return enabledKey;
}
function getBinary(client, key_id) {
return KEYS[client][key_id].binary;
}
module.exports = {
initKeys: initKeys,
validClient: validClient,
validKey: validKey,
getKey: getKey,
keyLength: guids.guidLength,
getBinary: getBinary
};

Source: https://codesandbox.io/s/simple-kms-579ts?file=/src/keybox.js

In our example, we use the initKeys function to load our keys into memory. We added the ability to apply specific keys to clients by adding aclient_id. Although this code still stores the crypt key as plain text in the database, I placed comments where you can take can add the extra step to decrypt an encrypted crypt key, and store it as thebinary attribute. Lastly, in order to allow us to rotate keys in and out of commission, we have the disabled attribute, notice the getKey function where we only return enabled keys for encryption.

Note: Rotating keys should be done on a regular intervals. It reduces the overall impact a potential breach can have. A compromised crypt key will only expose the data that was encrypted with it during your interval.

Data security is a constant arms race. We have to try to keep ahead of those who are looking for new and inventive ways or gaining access to our sensitive data, and KMS is just 1 tool, in the larger InfoSec toolbox but ultimately a core component of it.

Relevant Links:
Amazon KMS: AWS Key Management System.
Node.js Crypto API: https://nodejs.org/api/crypto.html

--

--