Asymmetric Public / Private Key Encryption (RSA) in Node.js
Published 2015-6-24Code Samples
- Generate x509 certs with WebCrypto
- forge: TLS in Javascript
- ursa: RSA in Node.js
- Creating X.509 Certificates with Web Crypto and PKIjs?
Summary
See https://github.com/coolaj86/examples-rsa-keypairs for code snippets in this post.
Asymmetric encryption is like a 1970's style cash lock box that can be locked by anyone using a publicly available store key, but that can only unlocked by the manager with the master key.
Asymmetric authentication is like a watermark or a signature. It's also like a lock that only the author can secure, but anyone can unlock.
These metaphorical lock boxes are very small - only big enough to store another key (meaning a symmetric key) or a fingerprint (meaning hashsum).
A use case for Public / Private crypto
Bob wants to send Alice a message, but he doesn't want Eve, the postwoman, to know what it is or to be able to tamper with it.
- Bob signs the message with his private key
- Bob uses Alice's public key to encrypt his message
- Bob gives the message to Eve to send to Alice
- Eve cannot verify if the message originated from Bob or someone else
- Eve cannot read the contents of the message (because it's encrypted)
- Eve cannot tamper with the contents of the message (because it's signed)
In reality this would be done in conjunction with message digests for signatures and symmetric keys for encryption, but the oversimplification above does the job for the basic premise.
In Summary
About Keys
- Public keys Encrypt & Verify
- Private keys Sign & Decrypt
About Signatures
- Created by Private key on a digest (i.e. a sha256 hashsum)
- Authentic
- A tamper-proof seal
- Completely visible
- Verifiable by anyone via Public key
About Crypto
- Encryptable by anyone with Public key
- Private
- Decryptable by recipient only
- You can start a two-way encrypted conversation with only one set of keys
Limitations
Asymmetric encryption is very slow and the size is very limited.
Protocols like HTTPS are very fast and can be used to encrypt very large streams of data because they only use RSA to exchange an symmetric AES key (a shared secret) and then continues the session with that shared secret.
Symantics
To "encrypt" and to "sign" are almost the same thing (as are "decrypt" and "verify" also very similar), but they are subtly different.
If you were to encrypt the string "hello", you would encrypt it with the recipient's public key and the string could be retrieved with the recipient's private key alone.
If you were to sign the string "hello" with your own private key, it can only be "verified" if the recipient has both the public key and the original plaintext.
You might intuitively think that you could simply "encrypt" data with your own private key to "sign" it, as the author. This would validate the authorship of the message, however, it if you intended to encrypt the message and send it with such a "signature", your "signature" would contain the message and it would now be publicly visible.
Securing Keys
Typically you may encrypt your private key with a passphrase-derived symmetric key.
Let's get (RSA) keys!
RSA is a standard for Public / Private cryptography. It's completely open source, patent and royalty free. That's why it works everywhere.
Here we're gonna create two 2048-bit RSA keypairs.
The reason we need two keypairs is that Bob and Alice each need to have their own keypairs.
Using OpenSSL.
# Make keys for Bob
mkdir -p ./bob
openssl genrsa -out ./bob/privkey.pem 2048
openssl rsa -in ./bob/privkey.pem -pubout -out ./bob/pubkey.pem
# Make keys for Alice
mkdir -p ./alice
openssl genrsa -out ./alice/privkey.pem 2048
openssl rsa -in ./alice/privkey.pem -pubout -out ./alice/pubkey.pem
Using node.js
npm install --save bluebird
npm install --save ursa
In short
var ursa = require('ursa');
var key = ursa.generatePrivateKey(1024, 65537);
var privkeypem = key.toPrivatePem();
var pubkeypem = key.toPublicPem();
console.log(privkeypem.toString('ascii'));
console.log(pubkeypem.toString('ascii'));
In full
rsa-keypairgen.js
:
'use strict';
var PromiseA = require('bluebird').Promise;
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path');
var ursa = require('ursa');
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
function keypair(pathname) {
var key = ursa.generatePrivateKey(1024, 65537);
var privpem = key.toPrivatePem();
var pubpem = key.toPublicPem();
var privkey = path.join(pathname, 'privkey.pem');
var pubkey = path.join(pathname, 'pubkey.pem');
return mkdirpAsync(pathname).then(function () {
return PromiseA.all([
fs.writeFileAsync(privkey, privpem, 'ascii')
, fs.writeFileAsync(pubkey, pubpem, 'ascii')
]);
}).then(function () {
return key;
});
}
PromiseA.all([
keypair('bob')
, keypair('alice')
]).then(function (keys) {
console.log('generated %d keypairs', keys.length);
});
Let's sign and encrypt some data
As stated before, you don't directly sign or encrypt data using RSA.
Instead you create a SHA-256 hashsum of the data and sign it with the sender's Private key.
Likewise you create a random 256-bit AES key and encrypt it with the recipient's Public key.
See http://crypto.stackexchange.com/a/9897 for a very simple explanation and http://www.czeskis.com/random/openssl-encrypt-file.html for a very effective demonstration.
Since the particulars of how to combine the RSA and AES operations and metadata into a particular flow and container format are defined in higher-layer standards - such as HTTPS, GPG, PKI, X.509 etc - we'll only encrypt a short message for this example.
You can think of this in a similar way to how mp4, MKV, avi, mov, flv, and others can all contain h.264 video and various types of audio.
With OpenSSL
echo "Hello, Alice!" > ./msg.txt
# Sign as Bob, so Alice knows who sent the message
openssl rsautl -sign -in ./msg.txt \
-inkey ./bob/privkey.pem -out ./msg.sig
# Encrypt the message and the signature with Alice's key
openssl rsautl -encrypt -inkey alice/pubkey.pem \
-pubin -in ./msg.txt -out ./msg.enc
With node.js
'use strict';
var fs = require('fs');
var ursa = require('ursa');
var msg;
var sig;
var enc;
var rcv;
// Bob has his private and Alice's public key
var privkeyBob = ursa.createPrivateKey(fs.readFileSync('./bob/privkey.pem'));
var pubkeyAlice = ursa.createPublicKey(fs.readFileSync('./alice/pubkey.pem'));
// Alice has her private and Bob's public key
var privkeyAlice = ursa.createPrivateKey(fs.readFileSync('./alice/privkey.pem'));
var pubkeyBob = ursa.createPublicKey(fs.readFileSync('./bob/pubkey.pem'));
msg = "IT’S A SECRET TO EVERYBODY.";
console.log('Encrypt with Alice Public; Sign with Bob Private');
enc = pubkeyAlice.encrypt(msg, 'utf8', 'base64');
sig = privkeyBob.hashAndSign('sha256', msg, 'utf8', 'base64');
console.log('encrypted', enc, '\n');
console.log('signed', sig, '\n');
console.log('Decrypt with Alice Private; Verify with Bob Public');
rcv = privkeyAlice.decrypt(enc, 'base64', 'utf8');
if (msg !== rcv) {
throw new Error("invalid decrypt");
}
rcv = new Buffer(rsv).toString('base64');
if (!pubkeyBob.hashAndVerify('sha256', rcv, sig, 'base64')) {
throw new Error("invalid signature");
}
console.log('decrypted', msg, '\n');
Encrypting with your Private Key
Encrypting with your private key and decrypting with your public key is nonsense. It's something you would only try to do if you're just learning about Public / Private key encryption and you still don't know what you're doing yet.
I'm including this section only because plenty of people, including myself, search for it.
WARNING: You don't understand what you're doing. Repeat: You can't get there from here. It don't do like that.
Let's say you're sending very small, twitter-sized messages. You might think that if you could encrypt them with your private key have others decrypt them with your public key that you save on bytes by only sending one encrypted message rather than the plaintext plus the 1024- or 2048-bit signature, effectively halving your size.
In that instance, you would be wrong. This cannot be done.
See also
- Khan Academy on Cryptography
- Khan Academy on RSA
- http://keybase.io
- [http://www.czeskis.com/random/openssl-encrypt-file.html](OpenSSL Commandline Examples)
- [https://stackoverflow.com/questions/9951559/difference-between-openssl-rsautl-and-dgst](rsutl: sign vs digest and sign)
- [https://github.com/Medium/ursa](node.js' uRSA)
- https://github.com/coolaj86/nodejs-self-signed-certificate-example
- https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless-Self-Signed-Certificates-in-node.js
- https://github.com/coolaj86/node-ssl-root-cas
- https://github.com/coolaj86/bitcrypt
- https://github.com/coolaj86/nodejs-ssl-trusted-peer-example
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 )