Symmetric Cryptography (AES) with WebCrypto and Node.js
Published 2015-6-26Yesterday I posted about Asymmetric Public Key Encryption and Signing (RSA).
Today I'm covering Symmetric encryption, also known as Shared-Secret Cryptography because, in practice, Asymmetric Encryption isn't very useful on it's own. It needs symmetric crypto to complete it.o
Web Crypto Examples Moved
Because the Web Crypto examples were so verbose, I didn't like putting them inline, so now they're at WebCrypto: Symmetric AES Encryption and Decryption)
Use Cases
The most common case for symmetric encryption is sharing data with someone that already has your key.
However, it's also useful for leveraging a service (i.e. dropbox) that you want to use for some benefit (i.e. cloud-storage) without actually sharing your data (i.e. your files) with the service.
Likewise, you might want to store a backup of authenticated key, session, or token on a remote system without disclosing it.
Asymmetric isn't good enough
- Really really really SLOW
- Limited, strict 128-, 256-, or 256-byte
- (that's 1024, 2048, or 4096 bits)
Asymmetric crypto has very strict byte constraints and is very slow.
The current standard for Public Key encryption (RSA) only allows messages of either 128, 256, or 512 bytes - and you're stuck with whatever you choose.
Even if you only need to encrypt the letter "a", it's gonna take a full 256 bytes. If you need to encode anything larger than a tweet or a text message, you'd have to create some sort of container format to concatonate messages together - and you'd still be losing several bytes in each packet to metadata and padding.
And did I mention that it's horrendously slow? (hint: yes, I did)
Note: Although RSA doesn't have initialization vectors, it has a functional equivalent with randomized padding so that you won't get the same output for the same input.
Creating and Sharing a Secret
A secret doesn't need to be a prime number or fill any requirements other than being cryptographically random in nature and either 128- or 256-bits wide.
Here's how I would get 256 random bits in node.js:
//
// AES Symmetric Key Generation in node.js
//
// Create a completely random secret
var sharedSecret = crypto.randomBytes(16); // 128-bits === 16-bytes
var textSecret = sharedSecret.toString('base64');
Note: You may wish to generate 32-byte (256-bit) keys as a general practice (so that they can be used for salt and as keys and as whatever else) because in places where only 16 bytes (128 bits) are needed, only 16 bytes will be used.
I could share this secret with my friend via medium with a secure end-to-end protocol and authentication that I trust For example, I trust gmail-to-gmail messages to be secure and authentic
128-bits IS good enough
There's no practical reason to use 256-bit encryption over 128-bit encryption.
Just spare yourself and use 128-bit encryption until Bruce Shneier says otherwise.
There are, of course, alternate views but in moving to encrypting all data (the peer web and home clouds), I see 256-bit encryption simply as a waste of CPU cycles that could be serving the user.
AES: Super-Fast Block-Ciphering
Here's how I could cipher data securely in node.js
//
// AES Symmetric Encryption in node.js
//
var crypto = require('crypto');
var sharedSecret = crypto.randomBytes(16); // should be 128 (or 256) bits
var initializationVector = crypto.randomBytes(16); // IV is always 16-bytes
var plaintext = "Everything's gonna be 200 OK!";
var encrypted;
cipher = crypto.Cipheriv('aes-128-cbc', sharedSecret, initializationVector);
encrypted += cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
// I would need to send both the IV and the Encrypted text to my friend
// { iv: initializationVector.toString('base64')
// , cipherText: encrypted
// }
Symmetric cryptography is based on the idea that you're able to share a secret with someone that you've already verified the identity of previously, or whose identity you can verify by other means.
You both use the same key for encrypting and decrypting.
Remember tha Public Key cryptography (RSA) is designed to be computationally intensive and easy to encrypt but hard to decrypt (except with the "trapdoor" of the private key).
Ciphers - such as AES - on the other hand, are designed much like hashsums (SHA-256) - to operate on arbitrary-length blocks of data as quickly as possible.
Asymmetric crypto's primary practical purpose in today's world is simply to exchange keys and signatures to be used by symmetric crypto.
Symmetric crytpo doesn't require prime numbers to be used for the key - any 128- or 256-bit (meaning 16- or 32-byte) number will do, so the entire 128-bits of key space are all valid keys.
Furthermore, although the math looks like something that would give you a heart attack, it's actually very simple - just obscure enough to make it practically impossible for an attacker to learn any details of the key even when the plain text and cipher text are both known.
Note that the Initialization Vector (IV) is a public bit of metadata that must be included with the data (generally appended as either the first or last 16-bytes of the message). The recipient must know this and it's fine for any attacker to know it as well.
You could leave the IV as 0, but then it would be possible for an attacker to tell if two blocks of data are the same. This could lead to a known plain text attack if they ever got unencrypted copies of messages.
Encryption Example: Your bank via HTTPS
When you connect to your bank, they present a public key with a signature from one of the HTTPS certificate vendors, which your browser already trusts (the files are literally built-in to the keychain of your browser and / or your operating system).
Your browser takes just a moment to generate a relatively "small" number - only 128-bits large - and then sends that back to your bank.
From that time forward all messages are encrypted with that AES key.
Then you exchange your personal credentials (username and passphrase) to verify your authenticity to them.
HMAC: One-to-One Authenticity
Hash-based Message Authentication is much simpler than it sounds.
It simply means to create a signature using a shared key - which means you're just taking a hashsum of data + the shared key.
You could think of it like this: HASH('SHA-256', secret, data)
.
In fact, that's only a few lines of code off from the actual algorithm.
It's a very simple mechanism to prove that a message was sent by one of two parties sharing the key.
var crypto = require('crypto');
var sharedSecret = crypto.randomBytes(32);
var signer = crypto.createHmac('sha256', key);
var plaintext = "Everything's gonna be 200 OK!";
var signature;
signer.update(plaintext);
signature = signer.digest('hex');
Signing Examples: Twilio & Mailgun WebHooks, Twitter OAuth 1.0a
When you sign up for a developer account on Twitter (you're already signed in), they issue you a "consumer secret". This is a shared key, primarily used for authentication.
Whenever you send an OAuth 1.0a request, you must alphabetize your parameters, hash them, and create an HMAC signature with your private key.
With Twilio and mailgun webhooks, the process is reversed. You receive an incoming message from mailgun via POST. That message contains a number of parameters, including a nonce, a timestamp, and the many parameters related to the message.
You use the API key (which may or may not satify the needs of an AES key) to sign the data you've received yourself. If the signature you generate does not match the signature sent to you, then it is not authentic (it may be a spam bot) and you should ignore the request.
Converting a Passphrase to a Key
Here's an example of turning an arbitrary-length passphrase into a secure psuedorandom key of an exact length:
var passphrase = "IT'S A SECRET TO EVERYBODY!";
// it is not necessary that this be private, just random on a per-user basis
var salt = crypto.randomBytes(32);
// you want this to slow down an attacker, but not yourself or a user
// if you use mobile devices or hobby hardware, keep it well under 10,000
// it is not necessary that this be private
var iterations = 137;
var keyByteLength = 32; // desired length for an AES key
crypto.pbkdf2(passphrase, salt, iterations, keyByteLength, 'sha256', function (err, bytes) {
console.log(bytes.toString('hex'));
});
There are many cases in which you would like to use a memerable passphrase in place of truly random key.
For example, you may want to encrypt your ssh private keys. There are also instances where you want to create a deterministic but reversable id (which rules out hashsums).
The solution for these is to use a key-derivation function such as PBKDF2, bcrypt, or scrypt. These algorithms are designed to be intensely slow - they're built by running an extremely inefficient hashsum hundreds or a thousand times instead of just once.
This makes it fast enough for you to create keys on an occasional one-off basis, but slow enough that you it increases the brute-force attack of your passphrase almost as secure as if it were truly random bits.
PBKDF2 has become part of the WebCrypto spec, so even though it isn't as CPU intensive and, unlike bcrypt, it could be optimized to run on bitcoin miners, GPUs, and other hashing hardware, it's now the most widely implemented key derivation function.
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )