Secure secret storage and cryptographic key retrieval for Node.js
var cryptex = require('cryptex');
cryptex.getSecret('mySQLPass').then(function(pass) {
conn = mysql.connect({
username: user,
password: pass,
host: hostname
});
});
If you check database passwords into git, download credential files from S3 or some other server, provide plaintext keys to your continuous integration/deployment solution, or don't have the ability to limit engineers from getting production secrets, stop doing what you're doing.
Cryptex is here to help. Here's how:
There are three ways to set up Cryptex. Use what works best for your project:
Set up cryptex in different ways automatically depending on what the NODE_ENV
environment variable is set to. The file looks like this:
{
"production": {
"keySource": "kms",
"keySourceOpts": {
"dataKey": "kms+encrypted+base64+string=="
}
},
"development": {
"keySource": "none",
"algorithm": "plaintext",
"secretEncoding": "utf8",
"secrets": {
"mySQLPass": "devlocal"
}
}
}
Don't want clutter in your file tree? That's cool. Do this:
cryptex.use({
config: {
keySource: 'kms',
keySourceOpts: {
dataKey: 'kms+encrypted+base64+string=='
}
}
});
Following 12 Factor? Rock on. We have env var support already built-in.
CRYPTEX_KEYSOURCE=kms
CRYPTEX_KEYSOURCE_KMS_DATAKEY="kms+encrypted+base64+string=="
Cryptex encrypts all of your secrets with a key. You don't want that key shared with anyone in plaintext, no matter how much you trust them. Cryptex will request your key when it's needed and delete it from RAM afterward. Here are the available sources:
Amazon Web Services' Key Management System is the most secure and easy-to-implement key source. If you already have an AWS account, KMS is cheap and easy to use. Create an encryption key using the IAM console, and note the alias you gave it. Install the AWS CLI tool and run this command with your key alias to get an encrypted AES256 key:
aws kms generate-data-key-without-plaintext \
--key-id alias/YOUR_KEY_ALIAS \
--key-spec AES_256 \
--output text \
--query CiphertextBlob
dataKey
CRYPTEX_KEYSOURCE_KMS_PATH
: The base64 string you got when you ran that command above.
region
CRYPTEX_KEYSOURCE_KMS_REGION
: The AWS region (such as us-east-1) in which the master KMS key can be found. If not specified, the config already loaded into aws-sdk is used.
A note about aws-sdk configuration: The KMS keySource uses Amazon's official Node.js aws-sdk library. If you're using npm>=3
, it will use the same object as any you might have in your local project, carrying over the configuration. Otherwise, please see Amazon's guide on configuring the SDK to provide it with credentials. The highly recommended way to allow it to access KMS in production (assuming it's in production on AWS servers) is to attach an IAM role to the EC2 node with permission to access the master key you're using.
If your secure key is available in a file, use this method. Note, however, that it is your responsibility to make sure that key file stays secure and inaccessible to prying eyes!
Is your key file something other than binary-encoded? Set keySourceEncoding
in your config, or set the CRYPTEX_KEYSOURCEENCODING
environment variable, to either base64
or hex
.
path
CRYPTEX_KEYSOURCE_FILE_PATH
: The path to the key file
DANGER. ONLY USE THIS IF YOU ABSOLUTELY KNOW WHAT YOU'RE DOING.
If you're using anything other than an https URL in production, you're definitely doing it wrong. You'll need to be an expert in locking your key server down for this to be anywhere near secure.
As with file
, if your key file is something other than binary-encoded, set keySourceEncoding
in your config, or set the CRYPTEX_KEYSOURCEENCODING
environment variable, to either base64
or hex
.
url
CRYPTEX_KEYSOURCE_HTTP_URL
: The URL to the key file to download
timeout
CRYPTEX_KEYSOURCE_HTTP_TIMEOUT
: The number of milliseconds after which to fail the download. (Default: 4000)
DANGER. SHOULD NEVER BE USED IN PRODUCTION.
Useful for local development and testing, this allows the key to be saved in plain text. You'll also want to set keySourceEncoding
in your config (or the CRYPTEX_KEYSOURCEENCODING
environment variable) to either base64
or hex
-- however you've stringified your key.
key
CRYPTEX_KEYSOURCE_PLAINTEXT_KEY
: Your key, in plain text
This is useful if you're plugging in an algorithm that doesn't require a pre-set key to be used.
The recommended and default algorithm is aes256
. If you're good with that, move on! You don't even need to set algorithm
in your config or the CRYPTEX_ALGORITHM
environment variable. But for the sake of completeness:
Military-grade symmetric encryption. The implementation in Cryptex computes a new random 128-bit initialization vector for each encrypted secret. Obviously, to use this, the key provided by your keySource must be a 256-bit AES key.
DANGER. SHOULD NEVER BE USED IN PRODUCTION.
Useful for local development. With this, no keySource is needed and all secrets can be stored in plain text. Remember to set secretEncoding
in your config, or the CRYPTEX_SECRETENCODING
environment variable, to utf8
.
If you installed Cryptex globally, you'll have a CLI tool called cryptex
that can encrypt and decrypt your keys according to your cryptex.json
or environment variables. It's this easy:
$ cryptex encrypt mypassword
Q+JfrQS5DtSjqWHu1oO4HqctA2hVw4VhaDQfBCuvO8U=
To specify a particular node environment (for cryptex.json
users), pass it in the -e
flag. Run cryptex --help
for all the details.
Provide your secrets to production by either putting them in your config like this...
// cryptex.json
{
"production": {
"keySource": "kms",
"keySourceOpts": {
"dataKey": "kms+encrypted+base64+string=="
},
"secrets": {
"mySQLPass": "Q+JfrQS5DtSjqWHu1oO4HqctA2hVw4VhaDQfBCuvO8U="
}
}
...or by saving them in uppercase environment variables prefixed by CRYPTEX_SECRET_
:
CRYPTEX_SECRET_MYSQLPASS="Q+JfrQS5DtSjqWHu1oO4HqctA2hVw4VhaDQfBCuvO8U="
First, grab an instance:
var cryptex = require('cryptex');
Creates a new Cryptex instance with the specified options. See the update
function below for an option list.
Encrypts the given data into a base64 string. If your string is binary data encoded as base64 or hex, just pass base64
or hex
for the encoding. The encoding is ignored if a Buffer is passed in.
Decrypts the given data and passes it back as utf8. If your string is binary data encoded as base64 or hex, just pass base64
or hex
for the encoding. The encoding is ignored if a Buffer is passed in.
Gets a Promise that resolves to a pre-saved secret, decrypted. See step 5 above. If no secret of the given name was found, this function with reject by default. To have it resolve with null instead, set optional
to true.
Gets a Promise that resolves to an object mapping the requested secret names to their decrypted values. Unlike calling getSecret
multiple times, this function will wait for the key to be cached (if enabled) from decrypting the first secret before decrypting the rest. This provides a faster and more efficient delivery of multiple secrets. If optional
is set to true
, any requested secrets not found will have their values in the object map set to null
. Otherwise, the returned Promise will be rejected.
Updates the cryptex instance with new configuration. Available options are:
file: The path to a json file to load, mapping environments names (as pulled from $NODE_ENV
) to configuration objects. Can also be set in CRYPTEX_FILE
. Defaults to cryptex.json
in the app process's current working directory.
env: The environment to select from the specified json file. Cryptex will attempt to pull an environment in this order: This value, the CRYPTEX_ENV
env var, the NODE_ENV
env var, or default to default
if all else has failed.
cacheKey: Boolean true to cache the key returned by the keySource in RAM, false to pull the key from the source every time it's needed. Can also be set in CRYPTEX_CACHEKEY
with "true" or "false". (Default: true)
cacheTimeout: If cacheKey is true, the number of milliseconds after which to delete the key and allow the Node.js garbage collector to remove it from RAM. Set to 0 to disable the timeout (NOT RECOMMENDED). Can also be set in CRYPTEX_CACHETIMEOUT
. (Default: 5000)
config: A configuration object specifying the keySource and other information outlined above. If this is set, Cryptex will not attempt to load a configuration file, and the environment
setting is ignored. Set config to {}
to force all configuration to be set in environment variables.
Get it global and local for the super convenient command-line tool:
npm install -g cryptex
npm install --save cryptex
Cryptex uses ES6 native Promises, available in Node.js version 0.12 and up.
The state of cryptographic security in Javascript is abysmal at best. Node lends itself to far better possibilities than what you'd find in the browser, but be aware of the following types of attacks:
- Javascript modules that require('cryptex') or some other portion of your code,
overriding
getSecret
or other functions to steal your decrypted private data. - Javascript modules that require('crypto') and monkey-patch the built-in Node library to steal private data.
- Nefarious applications that dump the RAM from your Node process. Node's garbage collector cannot be forced to run from Javascript, so even turning off the key cache could expose a window in which an unencrypted key could be stolen.
- Applications, server users, or javascript modules that read local key files. When in doubt, use Amazon KMS.
This article by NCC Group is from 2011 and focuses on the security of Javascript in the browser, but is still very much applicable today. However, by using Amazon KMS, Cryptex, and with careful review of all installed modules, a secure system in Node.js is possible.
Embassy supports Node 4 LTE and higher out of the box. For 0.12, consider compiling with Babel.
All original content is Copyright (c) 2015-2016 TechnologyAdvice, released under the ultra-permissive ISC license. See LICENSE.txt for details.