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)

git clone -b gui git@hykersec/hykersrc/micro-project-chat.git

cd micro-project-chat

npm install

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.

import request from 'request'

export default (id, name) => {
  return new Promise((resolve, reject) => {
    let url = 'https://github.com/' + name
      request(url, (error, resp, body) => {
        if (!(body || '').includes(id))
          reject(resp.statusCode != 200 ?
            'no such user' : 'id not in bio')
        else
          resolve()
    })
  })
}
1.2 Verify registration

We will put it to use right away, let's perform a verification when the user registers just to prevent him from typing his own username wrong.

In src/reg.js in the onVerify method wrap the props.onReg call in a call to above routine.

import bio from './src/bio.js'
...
bio(this.state.id, this.state.username).then(() => {
  this.props.onReg(this.state.id, this.state.username)
}).catch((err) => {
  this.setState({
    verifying: false,
    error: { verify: err }
  })
})

Logout, and verify that the check is working.

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.

// src/app.jsx
import bio from 'keytar'
import crypto from 'crypto'
...
componentWillUpdate(nextProps, nextState) {
  if (!this.state.id && nextState.id) {

    let id = nextState.id
    let password = keytar.getPassword('hyker-chat-app', id)

    if (!password) {
      password = nodeCrypto.randomBytes(48).toString('hex')
      keytar.addPassword('hyker-chat-app', id, password)
    }
  }
}

3. Integrate RIKS

HYKER provides a node wrapper for the RIKS C++ client library.

Let's add the package for easy integration into our electron project.

$ yarn add [email protected]:hykersec/riks-node.git

Let's start with instantiating RiksKit. componentWillUpdate seems to be a perfect fit to do this in.

// src/app.jsx
import RiksKit from 'riks-node'
...
componentWillUpdate(nextProps, nextState) {
  if (!this.state.id && nextState.id) {
    ...
    this.riksKit = new RiksKit(id, password, () => {})
  }
}

The empty lambda is the whitelist which we implement last.

3.2 Encrypt & decrypt

Then we can go ahead and inject the encrypt / decrypt calls.

encrypt belongs in the initTransport method in the transport.onMessage callback.

// src/app.js
...
this.transport.onMessage(({ channel, name, text }) => {
  // decrypt inserted
  this.crypto.decrypt(text).then(({ name, text }) => {
    ...
    this.state.messages[channel].push(
      new Message(name, text))
    ...
  })
})
...

And decrypt belongs in putMessage method.

// src/app.js
...
putMessage(text) {
  ...
  // encryption inserted
  this.crypto.encrypt({ name, text }).then(text => {
    this.transport.put(channel, name, text)
  })
}
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.

For every key request we use the same bio routine to verify our pal.

// src/app.js
this.riksKit = new RiksKit(id, password, (id) => {
  return new Promise((resolve, reject) => {
    this.transport.resolve(id).then((name) => {
      bio(id, name).then(resolve).catch(reject)
    })
  })
})

Now we are done!

Hopefully, you will now possess a seriously secure chat app. Thank you for following this guide.

Time to head over to hyker.io and create an account and start build something real!

HYKER.io

results matching ""

    No results matching ""