•6 months ago
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
android running before continuing.
Let's walk through and look at the important code needed for each step.
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
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/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:
The response payload has this shape:
apiKeyis the stream account identifier for your Stream instance. Needed to identify what account your frontend is trying to connect with.
tokenJWT 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:
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
And the layout:
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:
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:
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!
Step 2: List users
To keep things simple, we'll get all registered users from our backend and display them in a simple list view:
Here is the activity:
And the layout:
We make an API call via
BackendService.getUsers and filter the logged-in user out. We add the response to a simple
ArrayAdapter and display our results in a
ListView. When a user clicks on a list item, we start a
ChannelActivity, which is a 1:1 chat channel.
Step 3: Create a Private 1:1 Channel
First, we need to create our channel for our private chat. Let's look at our activity and layout:
And the layout:
We use off the shelf Stream UI components with two slight variations. First, we hook in a custom
EncryptedMessageInputView, which allows us to encrypt a message before sending it. We also hook in a custom
EncryptedMessageViewHolderFactory, which allows message decryption (we'll look at this in a bit). The essential bits start inside of
doAsync. First, we look up the other user's public key. This let's use encrypt our messages and verify their messages are authentic. Next, we create a channel in Stream via
.query. Once then channel is created, we load messages. Before we look at how we load messages, we need to send a message first.
Step 4: Sending an Encrypted Message
Let's look at
EncryptedMessageInputView, which is bound in the layout and configured in our activity. Here is the code:
We override Stream's
MessageInputView and simply decrypt the message before sending it.
prepareMessage before sending it to the API, so we override this and encrypt before sending the message along.
Please note that updating a message does not utilize this method, so be aware if you want to support edit functionality. You can consider building your own
MessageInputView. For brevity, we won't go there in this tutorial.
Step 5: Viewing Messages
Since all of our messages in Stream our now ciphertext, we need to decrypt them before displaying. To hook into Stream's UI components, we bind via
binding.messageList.setViewHolderFactory(EncryptedMessageViewHolderFactory(eThree)). With our custom factory, we can initialize a custom decryption object for each message:
This factory checks if we have a message type (vs. another type such as a date separator) and initializes our
EncryptedMessageViewHolder. Let's look take a look:
First, we check if we have a regular message, and if we do, we decrypt it. We copy our message object to avoid mutating the original version (Stream caches this, and we'll mess things up if we manipulate that). With that copy, we check if the message is ours or theirs. In our case, we know how to decrypt directly since we created it. If it's theirs, we need to look up their public key to verify the message. We pass this to Virgil and do our decryption.
Putting these last steps together, we'll see our final product:
And that's it! We now have a private and extremely secure end-to-end encrypted messaging app built for Android with Stream Chat and Virgil Security. You should have a full understanding of the basics of end-to-end encryption using Stream Chat and Virgil Security, along with a fundamental understanding of how the E2EE process works when implemented with Android, Virgil, and Stream Chat.
If you want to take it a step further, Stream Chat offers several resources to help you get to the next level with your Android chops. Check out the links below:
- Building an Encrypted, HIPAA Compliant Chatbot
- Build HIPAA Compliant Chat
- Android Chat Bubbles: Building iOS Style Chat in Android
Happy coding, and as always, please feel free to drop any thoughts or questions in the comments below!