- This is a end-to-end encrypted chat application.
- It uses public identites for recipient addresses.
- Its purpose is to demonstrate how to integrate HYKER functionality.
git clone git@github.com:hykersec/chat-demo.git
cd chat-demo
npm install
docker-compose up -d
npm start
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.
If you have any qestions of comments, please visit https://gitter.im/hykersec or open a issue.
This project comes pre-configured with HYKER free service. No account or other registration is needed.
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.gitbooks.io/docs/riks.html.
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 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
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.
node 6
python # 2 or 3
docker # and compose
electron-forge # npm install -g electron-forge
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 git@github.com:hykersec/chat-demo.git
cd chat-demo
npm install
The project contains a simple but complete chat UI with registration screen, channels and chat feed.
The GUI resides in the src
folder in the chat-demo
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 |
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 chat-demo
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 the backend:
docker-compose up -d
Launch the frontend:
npm start
Launch parallel apps by cloning the repo again in an other location.
To chat with a friend, have him do ssh -TNL 1337:localhost:1337 youruser@yourip
instead of docker-compose up
.
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.
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.
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.
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 contain 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 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.
Now we know enough to integrate HYKER into a existing data flow. With our strategy outlined above integration becomes straight forward. The app will interact with HYKER at three places:
- Whitelist
- Ecryption
- Decryption
There is one more piece of the puzzle, but technically HYKER out of scope:
- Id verification
Let's go through them one by one.
Let's start with id verification, since these functions are used when defining the whitelist.
const verifyEmailAccount = async (id, mail) => {
const url = `http://${CHAT_URL}/${CHAT_END_SHOW}/${mail}`
const body = await fetch(url).then(res => res.text())
return body.includes(id)
}
const verifyGithubAccount = async (id, username) => {
const url = `https://api.github.com/users/${username}?_=${+new Date}`
const data = await fetch(url).then(res => res.json())
if (!data.bio)
throw 'no such user'
return data.bio.includes(id)
}
Our whitelist is very simple in construction. For every incoming key request we will look through our entries.
If we find a match and the setting is ALLOW
, we proceed to verify the id. Otherwise, we reject the key request.
The whitelist is provided to the RiksKit
object through its constructor. Here we also construct our own id that
will be used in whitelists of other users.
// construct device id from name and random id
const deviceId = '#' + btoa(name + '|' + id).replace(/=/g, '')
const password = 'password' // mock password
// init the crypto
this.crypto = new RiksKit(deviceId, password, store, (did, ns) => {
// conveniently, the name (well-known) is part of id
const [ name, id ] = atob(did.substring(1)).split('|')
// deny if name not in whitelist
if (!this.state.whitelist[name])
return false
// verify relation between id and name
if (name.startsWith('gh:'))
return verifyGithubAccount(id, name.substring(3))
else
return verifyEmailAccount(id, name)
})
Encryption is super stright forward. Just pass the data through the encrypt
method before handing it over to the WebSocket.
publishMessage = async (text, channel) => {
text = await this.crypto.encrypt(text, channel) // inject encrypt
this.client.publish(channel, text)
}
this.client.onMessage(async ({ channel, sequence, name, text }) => {
text = await this.crypto.decrypt(text) // inject decrypt
this.state.messages[this.state.channel].push({ name, text })
})
Decryption is equally straigt forward. Just like before we pass the chiper text arriving from the WebSocket through the decrypt
method.
Thats all folks. Now you should know how to build a seriously secure chat app.
If you have any qestions of comments, please visit https://gitter.im/hykersec or open a issue.