In this tutorial, we'll build an encrypted chat/messaging example app for Android. To do this, we will combine the Stream Chat Platform and Virgil Security. Stream and Virgil make it easy to build a solution with excellent security by combining all of the features you would expect as a developer when creating a messaging app.

These two services allow developers to integrate chat that is zero knowledge to your backend or Stream. The example app embeds Virgil Security's eThree Kit – a secure encrypted messaging platform – with Stream Chat's Android components.

Note that all source code for this example Android app is available on GitHub. Additionally, before jumping into this tutorial, I recommend checking out our Android in-app messaging tutorial, which will walk you through how to implement Stream Chat from a high-level overview.

What Is End-To-End Encrypted Messaging?

End-to-end encrypted messaging means that the users within that specific chat can only read messages sent between two people. To enable this, the messages that are sent are encrypted before leaving a user's device, and can only be decrypted by the intended recipient (end-user).

Virgil Security is a vendor that allows developers to create end-to-end encryption via public/private key technology through the use of their robust and secure encrypted messaging service. With Virgil's Android SDK, developers can securely create, store, and provide robust end-to-end encryption.

During this tutorial, we will learn how to build a Stream Chat app that uses Virgil's encryption/decryption platform to prevent anyone except the intended parties from reading messages. No one in your company, nor any cloud provider you use, can read these messages. In essence, even if a malicious person gained access to the database containing the messages, that person would only see encrypted text, called ciphertext.

Building an Encrypted Chat Messaging Application

To build this app, we'll mostly rely on two libraries, Stream Chat Android and Virgil Security for Kotlin. Our outcome will encrypt text on the device before sending a message. Decryption and verification will both happen in the receiver's device. Stream's Messaging API will only see ciphertext, ensuring our user's data is never seen by anyone else, including us.

To accomplish this, the app performs the following steps:

A user authenticates with your backend. The user's app requests a Stream auth token and API key from the backend. The Android app creates a Stream Chat Client for that user. The user's app requests a Virgil auth token from the backend and registers with Virgil. This generates their private and public key. The private key is stored locally, and the public key is stored in Virgil. Once the user decides who they want to chat with the app creates and joins a Stream Chat Channel. The app asks Virgil for the receiver's public key. The user types a message and sends it to Stream. Before sending, the app passes the receiver's public key to Virgil to encrypt the message. The message is relayed through Stream Chat to the receiver. Stream receives ciphertext, meaning they can never see the original message. The receiving user decrypts the sent message using Virgil. When the message is received, app decrypts the message using the Virgil and this is passed along to Stream's UI components. Virgil verifies the message is authentic by using the sender's public key.

While this looks complicated, Stream and Virgil do most of the work for us. We'll use Stream's out of the box UI components to render the chat UI and Virgil to do all of the cryptography and key management. We simply combine these services.

The code is split between the Android frontend contained in the android directory, and the Express (Node.js) backend is found in the backend directory. See the README.md in each directory to see installing and running instructions. If you'd like to follow along with running code, make sure you get both the backend and android running before continuing.

Let's walk through and look at the important code needed for each step.

Prerequisites

Basic knowledge of Android (Kotlin) and Node.js is required to follow this how-to tutorial.

Note that this secure messaging app is intended only to run locally on your machine, and we will not be covering how to deploy the example app to iOS or Android app stores.

We use Anko to simplify our asynchronous code. Please note this library was recently deprecated. There are also likely bugs in our async implementation. However, we chose to keep the noise to a minimum in this tutorial by leveraging Anko and keeping async simple. Please use best practices for asynchronous code.

You will need an account with Stream and Virgil. Once you've created your accounts, you can place your credentials in backend/.env if you'd like to run the code. You can use backend/.env.example as a reference for what credentials are required. You also need to place your Stream API key in MainActivity.kt:60 .

Step 0. Setup the Backend

For our Android frontend to interact with Stream and Virgil, the application provides three endpoints:

POST /v1/authenticate : This endpoint generates an auth token that allows the Android app to communicate with /v1/stream-credentials and /v1/virgil-credentials . To keep things simple, this endpoint allows the client to be any user. The frontend tells the backend who it wants to authenticate as. In your application, this should be replaced with your API's authentication endpoint.

POST /v1/stream-credentials : This returns the data required for the Android app to communicate with Stream. In order return this info we need to tell Stream this user exists and ask them to create a valid auth token:

https://gist.github.com/nparsons08/b6dc1cc1a35056bc68e6c1a540a08079

The response payload has this shape:

https://gist.github.com/nparsons08/c2a77efaa4ed9ff8e6d15343a7f5f3ea

apiKey is the stream account identifier for your Stream instance. Needed to identify what account your frontend is trying to connect with.

token JWT token to authorize the frontend with Stream.

user : This object contains the data that the frontend needs to connect and render the user's view.

POST /v1/virgil-credentials : This returns the authentication token used to connect the frontend to Virgil. We use the Virgil Crypto SDK to generate a valid auth token for us:

https://gist.github.com/nparsons08/08946e3e8b9e78c7e6d0a1098677babb

In this case, the frontend only needs the auth token.

GET /v1/users : Endpoint for returning all users. This exists just to get a list of people to chat with.

Step 1. User Authenticates With Backend

First, we log in to a user. To keep things simple we'll just have an empty form that lets you log in with any name:

This is a simple form that takes any arbitrary name, effectively allowing us to log in as anyone. We set this up in our MainActivity :

https://gist.github.com/nparsons08/dcd23722a0863d5f12c20c22b4e66381

And the layout:

https://gist.github.com/nparsons08/2f3fea445a0c0e6af816ed2c27dd69de

When we submit the form, we sign into our backend, get a Stream and Virgil frontend auth token, generate our private key, and register with Virgil, then start our next activity. We'll look at each of these in turn.

Let's see our sign in and token generation:

https://gist.github.com/nparsons08/1f8d6e292f4e0c26763222affa5474ce

Since our backend (see Step 1) does the token generation, these are simple REST calls. The tokens returned are frontend auth tokens, which allow our client to talk to Stream and Virgil directly. Besides returning a list of users, we no longer need our backend to do any work.

Now that we have our frontend tokens, let's generate our private keys and register our public keys with Virgil:

https://gist.github.com/nparsons08/28bd219f0f8f93efe439925b40706d1a

Virgil's client is called eThree . We initialize an EThree instance and register. This call generates a private key and stores it on the device and sends our public key to Virgil. If we get a RegistrationException , we have already registered this user. Keep in mind, and you cannot log into the same user on a different device since we're not sharing the private key with the other device! This is possible, but out of scope for this tutorial. If you'd like to accomplish this, see Virgil's documentation.

Now that we have our tokens and registration let's find a user to chat with!