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
orDENY
.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!