IN THIS ARTICLE
As end-to-end encryption becomes the new normal in the world of chat, we want to showcase a PubNub-powered end-to-end encrypted chat that works across all client platforms and provides a way to protect the decryption key instead of relying on developers to store it themselves.
Using Virgil Security’s open source tech, this end-to-end encryption layer offers brand new in-browser and in-app key management, elliptic curve keys over 200x harder to break than RSA, and cloud-based key management service using Virgil Security’s open source tech. Because you deserve it – and your attackers, too.
Why should you care about end-to-end encryption when we’re already HIPAA-compliant?
We at PubNub know we’re the market leader in HIPAA-compliant chat and IoT communications, backed up by a whole system of security mechanisms. PubNub itself has both TLS/SSL and AES encryption baked in, but in this example, we’ve implemented a secure cloud service provider to handle protecting user data. We also apply the latest security innovations to protect your users’ privacy and stay up-to-date with every patch and practice possible.
But unfortunately, all of this can’t stop a developer from making errors that create vulnerabilities in your product or prevent the next malware attack from infiltrating data centers. That’s why we use end-to-end encryption, because it keeps data safe even if the systems around the data fail.
When you encrypt data on the client device, neither the cloud (aka data center) nor the developer has access to the decryption keys, which means a breach will just expose scrambled gibberish. Implementing end-to-end encryption as a fail-safe in imperfect, human-built software keeps you and your users safe.
In this tutorial, we’ll show you how to make PubNub’s ChatEngine live JavaScript app end-to-end encrypted with the Virgil Security SDK.
How does end-to-end encryption work?
When you type in a chat message, it is encrypted on your mobile device or in your browser and decrypted only when your chat partner pulls up the message in her chat window.
With end-to-end encryption the message remains encrypted:
- While it travels over Wi-Fi and the Internet
- Through your cloud data center’s web servers and backend servers
- In the database
- Through other servers en route to your chat partner’s mobile device
In other words, none of the networks or servers can see what the two of you are chatting about.
In contrast, if your app is only using HTTPS:
- Data is only encrypted as it goes over WiFi and the Internet, but unencrypted elsewhere.
- Your cloud vendor, data center, app developers, etc. can access your users’ plaintext chat messages on every server and in every database involved.
When cloud service providers add an extra layer of security by using “AT REST” encryption, it only means that the database file is encrypted on disk with a key that your cloud service provider can access. Hackers can capture that key and hack into the web or backend servers and capture all the data that’s going through them, or can hack into the live database and breach them out.
But what’s more likely is a data breach due to human error. A two year study in the UK found that 88% of data breaches were caused by developer error, not cyberattacks. If you don’t need have a strong business justification for needing to access user chat messages, end-to-end encrypting them could keep you out of trouble. Plus your users will love your product for the added privacy it offers.
Private & Public Keys
The underlying technology in end-to-end encryption is based on private and public keys:
Each user in the system has a public & private key pair:
- The public key is shared with all users in the system, almost like an phone number in a telephone directory. Anyone who wants to send you a message can look up your public key and encrypt a message that can only be decrypted by your corresponding private key.
- Your private key is secret to you and is used to decrypt data that’s been encrypted for you. If a public key is like a phone number in a telephone directory, the private key is like a PIN to access any messages sent to that number.
Virgil Security’s SDK and cloud service create the keys, share the public keys between users, and keep the private keys safe so that only the end user has access to it on their device. Neither Virgil Security nor PubNub have access to the private keys.
OK, let’s upgrade PubNub’s JS chat app in 5 minutes!
In the next few minutes, we’re going to make PubNub’s ChatEngine live JavaScript app end-to-end encrypted with Virgil Security:
Go to this CodePen and give the app a try by sending a couple of messages.
When you’re ready to begin the DIY end-to-end encryption upgrade, start by adding 4 includes:
- Click on Settings -> JavaScript
- Add the following 4 js includes by pasting them to the bottom of the list one-by-one:
https://cdn.jsdelivr.net/npm/virgil-crypto@3.0.0/dist/virgil-crypto-pythia.browser.umd.min.js https://cdn.jsdelivr.net/npm/virgil-sdk@5.0.0/dist/virgil-sdk.browser.umd.min.js https://cdn.jsdelivr.net/npm/virgil-pythia@0.2.2/dist/virgil-pythia.browser.umd.min.js https://cdn.jsdelivr.net/npm/@virgilsecurity/keyknox@0.1.1/dist/keyknox.browser.umd.min.js
- Close the Settings dialog.
Now, copy & paste this code block at the very beginning of the JS code file. This is all the plumbing to log in your users, “recover” your user private key and decrypt the PubNub channel key:
// Paste this code block at the beginning of the JS code file: const virgilCrypto = new VirgilCrypto.VirgilCrypto(); let channelKeyPair; // Identity of the pre-defined "signle" user of the chat const USER_IDENTITY = 'chatengine-demo-e2ee-user'; // The key under which the user's encrypted private key is stored in // the Virgil Keyknox service const USER_KEY_ID = 'chatengine-demo-e2ee-user-key'; // Prefix we will prepend to the ciphertext before sending the encrypted // message to be able to tell the encrypted and plaintext messages apart const ENC_MESSAGE_PREFIX = 'e2ee_by_virgil'; const initVirgil = async () => { // Get the JWT for authentication in Virgil APIs. Makes a request to the // server we've deployed for this demo. The Subject of the returned JWT will // always be equal to `USER_IDENTITY` const fetchVirgilJwt = async () => { const res = await fetch('https://virgil-pubnub-demo-chat-server.herokuapp.com/virgil-jwt'); if (!res.ok) throw new Error('Failed to get Virgil access token'); return await res.text(); }; // Get the pre-defined private key of the Chat Channel encrypted with the // user's public key. const fetchEncryptedChannelKey = async () => { const res = await fetch('https://virgil-pubnub-demo-chat-server.herokuapp.com/channel-private-key'); if (!res.ok) throw new Error('Failed to get encrypted channel key'); return await res.text(); }; const jwtProvider = new Virgil.CachingJwtProvider(fetchVirgilJwt); const brainKey = VirgilPythia.createBrainKey({ virgilCrypto, virgilPythiaCrypto: new VirgilCrypto.VirgilPythiaCrypto(), accessTokenProvider: jwtProvider }); // Derive the key pair from password. The password is hard-coded for demo // purposes only, it must be provided by the user in a real app. const passwordKeyPair = await brainKey.generateKeyPair('PubNubD3m0o'); // Setup the private key storage. const syncKeyStorage = Keyknox.SyncKeyStorage.create({ // this key will be used to decrypt the Cloud-stored keys privateKey: passwordKeyPair.privateKey, // this key is used to encrypt the Cloud-stored keys publicKeys: passwordKeyPair.publicKey, keyEntryStorage: new Virgil.KeyEntryStorage(), accessTokenProvider: jwtProvider }); // Synchronize the keys between the Virgil Cloud and local storage (IndexedDB) await syncKeyStorage.sync(); // Retrieve the pre-defined private key of the user const userPrivateKeyEntry = await syncKeyStorage.retrieveEntry(USER_KEY_ID); // Import to make it usable with `virgilCrypto` methods const userPrivateKey = virgilCrypto.importPrivateKey(userPrivateKeyEntry.value); // Retrieve the Chat Channel private key encrypted with the user's public key const encryptedChannelPrivateKeyData = await fetchEncryptedChannelKey(); // Decrypt with user's private key const channelPrivateKeyData = virgilCrypto.decrypt(encryptedChannelPrivateKeyData, userPrivateKey); // Import to make it usable with `virgilCrypto` methods const channelPrivateKey = virgilCrypto.importPrivateKey(channelPrivateKeyData); channelKeyPair = { privateKey: channelPrivateKey, publicKey: virgilCrypto.extractPublicKey(channelPrivateKey) }; };
Now, scroll down to the end of the JS code file and replace the PubNub init with this:
// boot the app initVirgil() .then(() => init()) .catch(err => console.error(err.message)); // boot the app -> old init, replaced with ^^ //init();
Encrypt Messages
Now that we have all the plumbing in the code, let’s encrypt some messages.
We’ll use a public key to encrypt messages in the browser before sending them into the channel, and we’ll download the channel’s public key from Virgil’s cloud service. In your own app, you should create different public keys for all your different channels (in this demo, we only have one channel).
Find the sendMessage =() code block in the JS code file and paste these lines at the beginning of the if() block:
// find the “if (message.length)” { line and paste this in the next line: // Encrypt the message with the Channel's public key message = virgilCrypto.encrypt(message, channelKeyPair.publicKey).toString('base64'); // Add prefix so the receiver can tell that this message is encrypted message = [ ENC_MESSAGE_PREFIX, message].join(':'); // Next line of code should be: myChat.emit( ‘message’, {
Now, try sending a message and see what happens. It turned into scrambled gibberish! If anyone breaches your system, this is what they’ll find. They won’t be able to decrypt the data because the private (decryption) keys never leave the client apps unless they’re in an encrypted form.
Decrypt Messages
To decrypt, we need to get our hands on the channel’s private key. To keep things truly end-to-end encrypted, we don’t simply store that key by itself. Instead, we store it encrypted with all the channel member users’ public keys, so that they can decrypt the channel private key with their private keys on their devices.
This is how we get that key:
- In the plumbing code (the first large block you pasted), we generate what we call a “BrainKey” from your user’s login password. (Remember that we have a password hard-coded in the sample.) A BrainKey is a private key that we derive from the user’s username + password at every login.
- Then we download your user’s encrypted private key from the cloud and decrypt it using the BrainKey. Your user’s private key will decrypt all channel keys to which you have access.
- Finally, we download the channel’s encrypted private key from the cloud and decrypt it with your user’s private key.
Feel free to read the steps one more time. It can be tricky. This key chaining technique ensures that messages in the channel are end-to-end encrypted and only specific users can decrypt them. User password -> unlocks user private key -> unlocks channel private key.
For demo purposes, we have one single user in the app with a hard-coded password. In your real app, we definitely recommend having multiple users, unless you’re one of those people who just likes to hear themselves talk. Oh, and *don’t* hard-code the password in your own app please.
Now that we have the channel key, we can use it to decrypt all messages in the renderMessage function.
First, find the scrollToBottom() call in your renderMessage function and paste this subfunction code after:
// Paste it right after the scrollToBottom(); call function tryDecrypt (message) { const [ prefix, ciphertext ] = message.split(':'); if (prefix === ENC_MESSAGE_PREFIX) { // The message seems to be encrypted try { // Decrypt and convert to string return virgilCrypto.decrypt(ciphertext, channelKeyPair.privateKey).toString('utf8') } catch (e) { // Return as is return message; } } return message;}
And the last thing to do – call the decrypt function. In the same function, replace the value of the messageOutput parameter with a tryDecrypt call:
let el = template({ messageOutput: tryDecrypt(message.data.text), // message.data.text, time: getCurrentTime(), user: message.sender.state });
That’s it! Give it a try.
In case you were lost along the way and just want to play with the final code, check out this CodePen.
Build It Into a Real App
To build this sample into a real production app, there are a few more steps you’ll need to do:
- Sign up for your own free PubNub and Virgil Security accounts.
- Create private and public keys for your users at signup. (In this demo, we pre-created a user for you and we logged in the same user for every visitor.)
- You’ll need to add code to your backend to keep your app secure:
- Publish user public keys to Virgil’s cloud service after users complete the signup process and you have validated them.
- Generate JWTs to authenticated users at login to grant them access to Virgil’s cloud services.
- When creating a new channel, create a new channel private & public key pair. Publish the public key in Virgil’s cloud and encrypt the private key with the participating users’ public keys. Store this encrypted private channel key in your own database. (In this demo, we pre-created your channel key and store it for you.)
- To invite a new user to the channel, you’ll need to be one of the users with access to the channel’s private key. To invite your new member, re-encrypt the channel’s private key with all participating users’ public keys, plus the new user’s public key. This way, she’ll have access to the channel’s private key. The end result is one single, encrypted key that multiple users can decrypt. If you have a ton of users in the channel, you may want to consider keeping a separate copy of all members’ channel keys to avoid the cost of re-encrypting the channel key in a weak browser/mobile device.
Final Thoughts
We like this end-to-end encryption solution example with Virgil Security’s technology because they have done the heavy lifting to build a simple and secure solution so that you can focus on your core product. This works across all client platforms (iOS, Android, web, server and IoT) and uses elliptic curve keys. Plus, with their cloud-based key management service, you no longer have to find a place to hide the decryption key yourself.
As you now know, most data breaches aren’t caused by weak security, but by developers making mistakes or poor decisions. They’re only human after all. With end-to-end encryption, those inevitable mistakes and bad decisions won’t be as costly because the encrypted data won’t be exposed in the case of a system breach. It’s the ultimate belt and suspenders security configuration that complements the other security mechanisms that PubNub already has in place. And your customers (and their Chief Security Officers) will love your product.
Virgil Security, Inc. enables developers to eliminate passwords & encrypt everything, in hours, without having to become security experts. Get started today at VirgilSecurity.com.