Table of contents

1op.eu - check this out. Source code here

The problem with encrypted chat applications nowadays

There are many great applications that do their best to ensure your privacy using advanced mathematics, like for example Signal besides many others.
The problem I’ve found is that all these solutions make you create an account, which is a time consuming task. For example: if you just want to send a secret password for your e-mail account to a friend and you don’t want anyone to find out, you will not both sign up just because of that. You need a quick and easy way to ensure that:

  1. No middleman knows what you are talking about. I’m talking about Server-side here, folks. We don’t even want a server to read our messages.
  2. No one can decrypt our messages later in time, even knowing our initial password. And that’s a tough one.

alt text

We need to go deeper…

I wanted to create a fairly simple chat application with ensured privacy. Of course, I could use AES encryption and encypt everything using a given password, but i didn’t want users that create weak passwords to be at risk of compromising their chat history later on. This is proof-of-concept secure chat application. Let’s give it our best.

The idea I’ve come upon is to use AES only for encrypting RSA Public Keys, and encrypt messages separately for each user later on. This ensures that even when the server gets compromised and our network gets spoofed, we still will be able to either see legitimate messages, or none at all (no middleman gets inside). And the same rogue middleman will not be able to decrypt our messages later on, even knowing the password that was entered initially. This solution does guarantee 100% privacy when a weak password is given, but only if we trust the server that it’s not trying to crack the password in real time. But we need to trust it at some point. Otherwise, you can read the code and set it up yourself. Or use stronger passwords, which probably is a better idea.

Backend side!

When it comes to the backend, I really wanted to use Phoenix Framework along with its Channels. I’ve enclosed the whole idea into this chunk of code (along with bunch of helper methods):

def join(...) do
  room_id
    |> ets_lookup
    |> sync_room(...)
    |> handle_response(...)
end

Server side is very simple. On init, it just starts the ETS :chatrooms table and for each user that connects to the socket, server does pattern matching to find out what the result of the lookup was:

defp sync_room([{room_id, sha512}], room_id, sha512), do: :ok
defp sync_room([], room_id, sha512) do
  :ets.insert(:chatrooms, { room_id, sha512 })
  :ok
end

defp sync_room(_, _, _), do: :error

So either the room exists and sha512 of the initial password matches or no such room is found. Otherwise (when the password’s sha512 does not match), it gives back an error.

Connected user sends its RSA Public Key, which is encrypted using AES with the given password. Server generates a random color for him/her and appends both to an Elixir socket for this user. Phoenix Presence keeps track of all connected users and as soon as new one connects it notifies everybody about the current state. New guy gets a channel state as soon as he/she connects.

Frontend magic!

Now I wanted this App to be fairly small, so I dropped the idea of using React/Redux, in which I would normally code this kind of App. As this is my private project, I wanted to experiment with something new. I’ve chosen Vue.js along with Vuex, because I’m an amazed by functional paradigm guy. :)

State, state, state

The idea is simple. We have a bunch of components and the state, in which we store messages and users. We also have a bunch of actions and mutations to modify our state as “purely” as we can. We dispatch actions in componenets and our application adapt to new state, because of Vue.js magic. This pretty much sums it up. Just like in React/Redux, but components are built differently. For example take a look at the Chat component:

<template>
  <div>
    <div class="row chat">
      <div v-if="messages.length > 0" class="column column-70">
        <transition-group name="list">
          <div v-for="message in messages"
             :key="message"
             :style="parseColor(message.user_color)"
             class="message">

            <div class="time_block"> {{ parseTime(message.date) }} </div>
            <em>{{ message.body }}</em>
          </div>
        </transition>
      </div>
      <div v-else>
        Write something! Don't be shy!
        <br>
        <div v-if="users.length == 1">
          ... and invite somebody, maybe? :)
        </div>
        <div v-else>
          <b>Oh! Somebody is here!</b> Write something!
        </div>
      </div >
    </div>

    <div class="container footer">
      <div class="row">
        <div class="user_count">
          users online: <b>{{ users.length }}</b>
        </div>

        <input
          maxlength="240"
          autofocus
          placeholder="(240 characters. Enter sends)"
          type="text"
          v-model="newMessage"
          v-on:keyup.13="sendMessage" />
      </div>
    </div>
  </div>
</template>

For such a small project, I’ve found applying this amount of logic to the HTML rather appealing.

Guess what. JavaScript code is in the same file:

<script>
import { mapState, mapActions } from 'vuex'

export default {
  name: 'chat',
  data () { return { newMessage: '' } },
  computed: mapState({
    messages: 'messages',
    users: 'users'
  }),

  methods: {
    ...mapActions(['SEND_MESSAGE']),

    parseColor (color) {
      // ...
    },

    parseTime (date) {
      // ...
    },

    sendMessage () {
      this.SEND_MESSAGE(this.newMessage)
      this.newMessage = ''
    }
  }
}
</script>

…and styles withing the same file, but please don’t make me paste them.

Notice the SEND_MESSAGE call. We call store’s actions inside the component.

Back to the store!

I have made a big action called REQUEST_ENTRANCE inside the store.js file that handles whole initial WebSocket connection (called from Entrance.vue component).

REQUEST_ENTRANCE ({dispatch, commit}, data) {
  const socket = openSocket()
  socket.onError(() => {
    commit('DISCONNECTED')
    commit('ALLOW_SUBMIT_ENTRANCE')
  })

  socket.onOpen(() => {
    const rsa = new JSEncrypt({default_key_size: 2048})
    commit('SAVE_RSA', rsa)

    const encryptedRsaPub = AES.encrypt(rsa.getPublicKey(), data.password).toString()

    const channel = prepareChannel({
      socket,
      encryptedRsaPub: encryptedRsaPub,
      roomId: data.room_id,
      password: data.password
    })

    channel.join().receive('ok', () => {
      commit('REMOVE_ERROR')
      commit('ALLOW_SUBMIT_ENTRANCE')
      commit('SAVE_CREDENTIALS', data)
      commit('SAVE_CHANNEL', channel)

      dispatch('HOOK_CHANNEL', channel)
      dispatch('SYNC_HREF_WITH_ROOM_ID')
    }).receive('error', (res) => {
      commit('DISCONNECTED', res['reason'])
      commit('ALLOW_SUBMIT_ENTRANCE')
    })
  })
},

It’s pretty self-explanatory. Reads almost as if it was written in english (or Ruby). :)

Phoenix Presence JavaScript file!

HOOK_CHANNEL in above code snippets is a call to this action:

HOOK_CHANNEL ({state, dispatch, commit}, channel) {
  channel.on('new_msg', payload => commit('RECEIVE_MESSAGE', payload))

  channel.on('presence_state', initial => {
    dispatch('UPDATE_PRESENCE', Presence.syncState(state.presence, initial))
  })

  channel.on('presence_diff', diff => {
    dispatch('UPDATE_PRESENCE', Presence.syncDiff(state.presence, diff))
  })
},

All thanks to phoenix.js file that I copied from the Phoenix Framework. That is how I manage track of users connected. And that’s where the magic happens.

Every message sent is handled by SEND_MESSAGE action:

SEND_MESSAGE ({state, commit}, message) {
  const encMessage = state.users.map((user) => {
    let encrypt = new JSEncrypt()
    encrypt.setPublicKey(user.rsa_pub)

    return [
      user.user_color,
      encrypt.encrypt(message)
    ]
  })

Server-side splits received JSON and sends every user message encrypted especially for him/her. That’s where the channel.on hook fires up the RECEIVE_MESSAGE action:

RECEIVE_MESSAGE (state, message) {
  state.messages.push({
    user_color: message.body[0],
    body: state.rsa.decrypt(message.body[1]),
    date: new Date()
  })
}

Message gets decrypted using your own RSA Public Key and Chat components rerenders. :)

What next?

I probably will not try to monetize this project and I don’t want to spend a single minute on trying to learn marketing nuances. I just wanted to have a cool project to put into the portfolio and I wanted to solve some real life problem, which is an easy to use end-to-end realtime encryption in web chat application. (That’s a real life problem if you are still wondering)