Micro project: seriously secure chat app.

Abstract

In this micro project we will use HYKERs RIKS client to create a seriously secure instant messaging desktop application.

The project is more than an example of how to build an encrypted chat application, it highlights important problems and solutions e.g. id management (aka how to know you talk to the correct person).

RIKS is an abbreviation for "Retroactive Interactive Key Sharing" and is HYKERs protocol and toolkit for implementing secure communication in systems that are distributed, dynamic and asynchronous.

HYKER service

This project comes pre-configured with HYKER free service. No account or other registration is needed.

Prior knowledge

Readers that are new to the area are encouraged to look through the HYKER developer program (2 hours), or at least the included article about id management (20 min); hyker.gitbooks.io/developer-program. For introduction to RIKS, have a look at hyker.io/docs.

Identities

HYKER RIKS makes encrypted communication in environments such as chat applications simple. RIKS also makes id management simpler by dealing with cryptographic keys and digital signatures, and let you work with any string to represent a recipient. However, the application still has to make sure the correct string representations are used.

This will ensure that your service providers cannot access your users information, only handle their data.

There are many ways to achieve this:

One way to ensure correct identities is to use something well-known as id, such as an email address. This is however impractical, because one would rather not revoke a well-known id if e.g. a device is lost.

Another impractical way would be to meet your recipients in person and exchange identity information.

A more practical solution might be to use a central trusted entity such as a company db.

Another practical solution would be to use publicly verifiable identities, using e.g. social media platforms.

In this project

In this project we will explore a solution combining a well-known id (representing a user) with a random unique id (representing a users device). These identities will be linked together, and the connections will be publicly verifiable. To do this, we will use GitHub as a central trusted entity. We will trust Github not to fiddle with our profiles, and to ensure that only we have access to our account. We will also build one central trusted entity ourselves, similar to a company db that contains of email addresses of employees.

That is, we will use GitHub usernames and email addresses as recipients in our chat app, but decouple them from individual devices allowing us to revoke them should we want to (or allow a user to have multiple devices).

For each chat client, we will generate a random string and use it to identify that device. Then we ask the user to publish it on their GitHub profile page, or register it for them to the company db. The later will require authentication though a clicking a link in a verification email.

Later when someone wishes to decrypt our messages, we can grant them access (RIKS shares our key) after verifying their seemingly random identity using GitHub or the company db.

This can be done through a company db lookup or, a simple http request against github.com to find out if that id actually beings to a user account name we trust (if it's present in their profile).

This check can be done super simple yet securely since github.com uses ssl with a server certificate issued by an authority that our computers already trusts, making outsiders unable to intercept and alter our checks.

This flow highlights an important and desirable property of HYKERs RIKS protocol, messages are distributed first and access is granted later.

This system possesses all characteristics we desire. The only ways we could get into trouble would be if the user fails to remember his or hers pals github usernames or company email addresses correctly, or if github decides to become evil. A solution to the latter problem would be to use multiple trusted entities to verify the id of our pals devices, but we won’t do that in this example. There actually exists a service doing this called keybase.io

Let's get to it

We will use RethinkDB for message transport and Electron as application platform. React will be used for view components and application state. Redis will power the company db.

Prerequisites

node 6

docker

electron-forge # npm install -g electron-forge

Frontend

A simple chat GUI has been put together for your convenience. (It was created with electron-forge init my-new-project --template=react)

The project contains a simple but complete chat UI with registration screen, channels and chat feed.

Quick overview

The GUI resides in the src folder in the micro-project-chat project.

File

Type

Responsibility

src/app.js

app

entry point, app state

src/login.js

view

display login at first launch

src/channels.js

view

display channels in left column

src/chat.js

view

display chat feed and input box

src/client.js

logic

interact with backend

src/verify.js

logic

verify identities

Backend

A simple chat backend powered by node express and RethinkDB has been put together for your convenience. The backend handles realtime events related to chat functions as well as implements the company db.

Above project contains a simple docker setup containing all dependencies of the backed.

The backend resides in the server/app.js folder in the micro-project-chat project.

Function

Type

Domain

DB

Action

add

api route

Chat

Memory

Add member to channel

del

api route

Chat

Memory

Remove member from channel

get

api route

Chat

Memory

Get channels of member

reg

api route

Co. DB

Redis

Register id with email address

verify

api route

Co. DB

Redis

Verify mail account by code

show

api route

Co. DB

Redis

Get ids registered to email

wait

ws route

Co. DB

Redis

Wait until id and mail are verified

pull

ws route

Chat

Rethink

Incoming chat events e.g. pub & sub

push

ws push

Chat

Rethink

Push chat messages to clients

notify

ws push

Chat

Memory

Notify clients of changes

insert

logic

Chat

Rethink

Insert message into storage

listen

logic

Chat

Rethink

Listen for new messages in the db

select

logic

Chat

Rethink

Select messages from the db

fetch

logic

Chat

Rethink

Fetch messages since a given date

getInfo

util

Chat

Memory

Get channels of member

sendMail

util

Co. DB

Send emails powered by MailGun

Launch

Launch the backend:

docker-compose up

Launch the frontend:

npm start

Launch parallel apps by cloning the repo again in an other location.

End-to-end encryption

Now it is time to add encrypted channels to the chat app. Before this doing this, let's look at some concepts and limitations.

In this example we have chosen a design where communication is one-to-many, but trust is one-to-one (and possibly one-way).

That means that a user can join any channel and start to put messages into it. All other users subscribing to that same channel will now receive those messages. However, this does not mean that they can read (decrypt) them.

Whitelist

For that to happen, a trust relation must be established. Luckily, with RIKS, this is simple; all that must be done is to put my trusted pals usernames into a whitelist that is kept local in the chat app.

Key request and response

When some device receives my message, RIKS will send a key request to my device. If his identity is present in my whitelist, a key response will occur and he can decrypt the message.

Well-known user id vs. unique device id

RIKS operates on the device level and only knows about identities of devices. However, we would like to use well-known identities everywhere in our app since these are the ones we recognise. This means out whitelist will contains well-known user identities.

But when a key request arrives, it will reference one of those unique strings representing the device. This is why we are clever when constructing the id. We start of with the well-known identity and prepend a random unique sufix. This way, when we handle a key request we instantly know both the device id and which user it belongs to. All that is left is to verify the connection between the two using the method described earler.

Limitations

Limitations to this example:

The whitelist implementation is simplistic. Entries are automatically added to the whitelist as the app becomes aware of new identities. The user may then set the value of a entry to ether ALLOW or DENY.

ALLOW is the default value of a new entry if the app discovers the identity as the user explicitly adds it to a channel.

DENY is the default value of a new entry if the app discovers the identity from any other way e.g. some other user adds itself to a some channel.

In this example we will implement reactive access control, meaning sharing keys upon request. However, one may easily implement proactive access control which involves preshare of keys.

1. Identity verification

We have earlier talked about using GitHub for identity verification, let's implement it!

Let's start by creating a new file src/bio.js that will handle http requests to GitHub for id verification. This simply means to fetch the user's profile and look for the id somewhere in the body.

2. Generate and store password

RIKS client library needs to store certain files on disk. To do that securely it uses password based encryption, and therefore a password must be specified when creating the RiksKit object.

One could ask the user for a password each time they launch the app, but a more convenient way is to generate a password and store it in the os keystore.

To makes this simpler we use a package called keytar that solves it all.

Add keytar it to our project.

$ yarn add keytar

As soon as we have got our id we can generate a password using the keytar api and store it in the os. Let's use a common React pattern and introduce a method called componentWillUpdate and look for a change of state.id.

3.2 Whitelist

Finally, it's time to implement the whitelist. To do the id verification we need the potential pals username. Luckily we have already prepared this step by inserting this information in our RehinkDB right after registration. We can now use the transport abstraction to resolve his id into a username.