Free up to 1MM monthly messages. No credit card required.
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.
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.
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:
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:
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.
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:
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.
In the next few minutes, we’re going to make PubNub’s ChatEngine live JavaScript app end-to-end encrypted with Virgil Security:
When you’re ready to begin the DIY end-to-end encryption upgrade, start by adding 4 includes:
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
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();
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.
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:
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.
To build this sample into a real production app, there are a few more steps you’ll need to do:
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.
A Notice of Privacy Practices (NPP) is one of the requirements of HIPAA and helps patients understand their personal data rights.
Michael Carroll
HIPAA violations can be financially expensive and devastating to a brand. Examine some examples of HIPAA violations, and learn...
Michael Carroll
HIPAA covered entities must follow the five technical safeguards to achieve HIPAA compliance and prevent data corruption.
Michael Carroll