Badu Boahen

Per-User Encryption in Elixir Part II

In Part I, we looked at how we’ll design a per-user encryption system. With the idea from Part I, let’s create our Phoenix project and see how we can accomplish per-user encryption.

Creating a phoenix project

    $ mix phx.new user_encryption

Libsodium

Cryptography is hard and it’s definitely not a good idea to roll your own library when doing crypto in your project. It’s better to use a tried and tested solution. In this project, we’ll use Libsodium. As described on its project page:

Sodium is a new, easy-to-use software library for encryption, decryption, signatures, password hashing and more. It is a portable, cross-compilable, installable, packageable fork of NaCl, with a compatible API, and an extended API to improve usability even further. Its goal is to provide all of the core operations needed to build higher-level cryptographic tools.

It also has bindings for most programming languages making it ideal for working on a project that requires encryption and decryption across different components, say frontend using libsodium Javascript library and backend using Elixir library.

To use Libsodium in our Elixir project, we’ll make use of enacl — Erlang bindings for libsodium.

Installing libsodium

Installing libsodium is fairly simple. Check here for detailed instructions.

On my Mac, this is how I installed it

  1. Downloaded the latest release from https://download.libsodium.org/libsodium/releases/LATEST.tar.gz

  2. Extracted the zip file and navigate to the directory containing the extracted files

  3. Run these commands

   $ ./configure
   $ make && make check
   $ sudo make install

If you got libsodium installed successfully, let’s continue.

Add dependencies to phoenix project

    #mix.exs
    defp deps do
        [
            {:comeonin, "~> 3.0"},
            {:enacl, git: "https://github.com/jlouis/enacl"}
        ]
    end

For convenience, we’ll also add comeonin for hashing and verifying user passwords. Libsodium can do that but that’s not the focus of this post. So we’ll use comeonin for hashing and verifying user passwords and use enacl for key generation, encryption, and decryption components of the app.

Install phoenix project dependencies

$ mix deps.get

Database

To keep it simple, our project will have two tables — users and user_data. Users table will store information on users while user_data stores the encrypted data for our users.

Let’s generate the User and UserData models in our project.

    $ mix phx.gen.context Account User users email:string:unique password_hash:text name:string key_hash:text
    $ mix phx.gen.context Account UserData user_data user_id:integer data_hash:text

The above commands will generate the following files in addition to 2 migration files in your priv/repo/migrations directory

lib/user_encryption/account/account.ex
lib/user_encryption/account/user.ex
lib/user_encryption/account/user_data.ex

Modify the User Model

Modify the lib/user_encryption/account/user.ex to take care of password hashing and generating key hash for new users.

#lib/account/user.ex

defmodule UserEncryption.Account.User do

      use Ecto.Schema
      import Ecto.Changeset

      schema "users" do
        field :email, :string
        field :key_hash, :string
        field :name, :string
        field :password_hash, :string
        #add virtual password field
        field :password, :string, virtual: true
        timestamps()
      end

      def changeset(%{id: nil}=user, attrs) do
        user
        |> cast(attrs, [:name, :email, :password])
        |> validate_required([:name, :email, :password])
        |> unique_constraint(:email)
        |> put_password_hash()
        |> put_key_hash()
      end

      def changeset(user, attrs) do
        user
        |> cast(attrs, [:name, :email, :password, :key_hash])
        |> validate_required([:name, :email])
        |> unique_constraint(:email)
        |> put_password_hash()
      end

      def put_password_hash(%Ecto.Changeset{valid?: true, changes: %
         {password: password}}=changeset)do
        password_hash = Comeonin.Bcrypt.hashpwsalt(password)
        changeset
        |> put_change(:password_hash, password_hash)
      end

      def put_password_hash(changeset)do
        changeset
      end

      def put_key_hash(%Ecto.Changeset{valid?: true, changes:
         %{password: password}}=changeset)do
          %{key_hash: key_hash}=
          UserEncryption.Security.Utils.generate_key_hash(password)
         changeset
         |> put_change(:key_hash, key_hash)
      end

      def put_key_hash(changeset)do
       changeset
      end

end

Create the database and run migrations

$ mix ecto.create
$ mix ecto.migrate

Security Module

Now let’s add lib/user_encryption/securty/utils.ex file that will contain functions for key generation, encryption, and decryption of data.

# lib/user_encryption/securty/utils.ex
defmodule UserEncryption.Security.Utils do

    #Public API functions
     def generate_key_hash(pwd)do

       #derive a key from the password
       %{key: password_derived_key, salt: salt} = derive_pwd_key(pwd)

       #generate unique key
       user_key = generate_key()

       #encrypt unique key with password derived key
       encrypted_user_key = encrypt(password_derived_key, user_key)

       #return encrypted key with the salt
       %{key_hash: salt <> "$" <> encrypted_user_key}

    end

    def update_key_hash(old_password, key_hash, new_password)do
       user_key = decrypt_key_hash(old_password, key_hash)
       %{key: password_derived_key, salt: salt} = derive_pwd_key(new_password)
       new_encrypted_key = encrypt(password_derived_key, user_key)
       #combine hash and encrypted key to get key hash
       %{key_hash: salt <> "$" <> new_encrypted_key}
    end

    def decrypt_key_hash(pwd, key_hash)do

      #extrack salt and encrypted key from key hash
      [salt, encrypted_user_key] = key_hash |> String.split("$")
      password_derived_key = derive_pwd_key(pwd, salt_string_to_bin(salt))
      {:ok, user_key} = decrypt(password_derived_key, encrypted_user_key)
      user_key
    end

    def decrypt(key, data)do
      decrypt(%{key: key, payload: data})
    end

    def encrypt(key, data)do
      encrypt(%{key: key, payload: data})
    end

    #PRIVATE FUNCTIONS

    defp encode(d)do
        Base.encode64(d)
      end

    defp decode(d)do
        Base.decode64(d)
      end

    defp generate_salt_bin()do
        :enacl.randombytes(16)
      end

    defp generate_salt_string()do
        generate_salt_bin()
        |> encode()
      end

    defp salt_string_to_bin(salt) do
        {ok, data} = decode(salt)
        data
      end

    defp derive_pwd_key(pwd, salt) do
        case :enacl.pwhash(pwd, salt)do
         {:ok, h}-> h |> encode()
         other -> other
        end
      end

    defp derive_pwd_key(pwd)do
        salt = generate_salt_bin()
        %{salt: encode(salt), key: derive_pwd_key(pwd, salt)}
      end

    defp generate_key()do
        key = :enacl.randombytes(:enacl.secretbox_key_size)
        encode(key)
      end

    defp encrypt(%{key: key, payload: payload}) do
        key_size = :enacl.secretbox_key_size
        {:ok, <<d_key::binary-size(key_size)>>} = decode(key)
        nonce = :enacl.randombytes(:enacl.secretbox_nonce_size)
        ciphertext = :enacl.secretbox(payload, nonce, d_key)
        encode(nonce <> ciphertext)
      end

    defp decrypt(%{key: key, payload: payload}) do
        key = decode_key(key, :enacl.secretbox_key_size)
        nonce_size = :enacl.secretbox_nonce_size
        {:ok, <<nonce::binary-size(nonce_size), ciphertext::binary>>} =
        decode(payload)
        :enacl.secretbox_open(ciphertext, nonce, key)
      end

    defp decode_key(key, key_size) do
        {:ok, <<secret_key::binary-size(key_size)>>} = decode(key)
        secret_key
    end

end

The module has 5 API function : encrypt/2, decrypt/2, generate_key_hash/1, update_key_hash/3, decrypt_key_hash/2

The most important ones for our purpose are the last 3 generate_key_has/1 — Takes a user’s password and generates key_hash for the user. The key hash consists of unique salt and the encrypted user key separated by $. The result of this function will be saved as the user’s key_hash in our database. decrypt_key_hash/2 — Takes in user password and key_hash and returns the user key. This function will be used on login to decrypt the key hash to get the user key that’ll be used to encrypt and decrypt the user’s data. update_key_hash/3 — Takes old_password, key_hash and new_password and returns a new key hash for the user. This function is necessary to allow the user to update the password and still be able to access data encrypted with their old password. Remember we only use the password to generate a password derived key to encrypt the user key, not the user data. So when changing passwords, we only need to use the new password to encrypt the user key again to generate the new key_hash.

Application Routes

Our simple app will have 6 routes

/register — To register a new user /login — To login users /logout — To logout users /encrypt — To encrypt user data /decrypt — to decrypt user data /changepassword — To allow the current user to change password

Routes /encrypt , /decrypt and /changepassword will require that a user is logged in. We will use a middleware — UserEncryptionWeb.ValidUserPlug — to ensure that only logged-in users have access to those routes.

When a user logs in, we will store the user details in the session. Our middleware will check the presence of a user in the session. If not present we redirect the request to the /login route.

Modify routes in lib/user_encryption/route.ex

defmodule UserEncryptionWeb.ValidUserPlug do
    import Plug.Conn
    def init(opt \\ [])do
        opt
    end

    def call(conn, opt)do
        case get_session(conn, :user)do
            nil ->
            conn
            |> Phoenix.Controller.redirect(to: "/login")
            |> halt()
            _ ->
            conn
        end
    end
end

defmodule UserEncryptionWeb.Router do
    use UserEncryptionWeb, :router

    pipeline :browser do
        plug :accepts, ["html"]
        plug :fetch_session
        plug :fetch_flash
        plug :protect_from_forgery
        plug :put_secure_browser_headers
    end


    pipeline :valid_user do
        plug UserEncryptionWeb.ValidUserPlug
    end

    scope "/", UserEncryptionWeb do
    pipe_through [:browser , :valid_user]
    get "/encrypt", PageController, :encrypt
    post "/encrypt", PageController, :do_encrypt
    post "/decrypt/:id", PageController, :do_decrypt
    get "/decrypt/:id", PageController, :do_decrypt
    get "/decrypt", PageController, :do_decrypt
    get "/changepassword", PageController, :change_password
    post "/changepassword", PageController, :do_change_password
    end

    scope "/", UserEncryptionWeb do
    pipe_through :browser # Use the default browser stack

    get "/register", PageController, :register
    post "/register", PageController, :do_register
    get "/login", PageController, :login
    post "/login", PageController, :do_login
    get "/logout", PageController, :logout
    post "/logout", PageController, :logout
    get "/", PageController, :index
    end

end

Registering a new user

Add the 2 functions — register/2 and do_register/2 to lib/user_encryption/controllers/page_controller.ex register/2 will display registration form and do_register/2 will handle registration itself.

   #lib/user_encryption/controllers/page_controller.ex
    def register(conn, _params)do
      render conn, "register.html"
    end

    def do_register(conn, %{"name"=>name, "email"=>
      email,"password"=>password,"confirm_password"=>confirm})do

      case UserEncryption.Account.create_user(%{name: name,
        email: email, password: password}) do
         {:ok, u}->
           conn
           |>clear_session()
           |>redirect(to: "/login")
         _ ->
          conn|>redirect(to: "/register")
      end
    end

Add registration template file lib/user_encryption/templates/page/register.html.eex

<div class="row marketing">
  <form name="register" method="post">
    <h4>Register</h4>
    <input
      type="hidden"
      name="_csrf_token"
      value="<%= Phoenix.Controller.get_csrf_token() %>"
    />
    <input placeholder="Name" name="name" type="text" class="field" />
    <input placeholder="Email" name="email" type="email" class="field" />
    <input
      placeholder="Password"
      name="password"
      type="password"
      class="field"
    />
    <input
      placeholder="Confirm Password"
      name="confirm_password"
      type="password"
      class="field"
    />
    <input type="Submit" value="Register" class="field" />
  </form>
</div>

Login

Add the 2 functions — login/2 and do_login/2 to page_controller.ex login/2 will display the login form and do_login/2 will handle the login itself. It takes in password and email, validates the user, and stores the user key in session.

    def login(conn, _params)do
       render conn, "login.html"
    end

    def do_login(conn, %{"email"=>email, "password"=>password})do
      user = UserEncryption.Account.get_user_by_email(email)
      case UserEncryption.Account.validate_user(user, password)do
        %{key: key}->
          conn
          |>put_session(:key, key)
          |>put_session(:user, user)
          |>redirect(to: "/encrypt")
        _ ->
          conn |> redirect(to: "/login")
      end

    end

Add login template file lib/user_encryption/templates/page/login.html.eex

<div class="row marketing">
  <form name="login" method="post">
    <input
      type="hidden"
      name="_csrf_token"
      value="<%= Phoenix.Controller.get_csrf_token() %>"
    />
    <input placeholder="Email" name="email" type="email" class="field" />
    <input
      placeholder="Password"
      name="password"
      type="password"
      class="field"
    />
    <input type="Submit" value="Login" class="field" />
  </form>
</div>

Add get_user_by_email/1 and validate_user/2 to lib/user_encryption/account/account.ex

validate_user/2 takes in user and password, verifies the password and returns the user key.

    def get_user_by_email(email)do

      query = (from u in User, where: u.email == ^email)
      Repo.one(query)

    end

    def validate_user(%{password_hash: password_hash,
        key_hash: key_hash}, password)do
        alias UserEncryption.Security.Utils

       case Comeonin.Bcrypt.checkpw(password, password_hash)do
          true->
           %{key: Utils.decrypt_key_hash(password, key_hash)}
          false ->
            false
        end

    end

Encrypting user data

Add 2 functions — encrypt/2 and do_encrypt/2 to lib/user_encryption/controllers/page_controller.ex

encrypt/2 shows a form with a field to enter data to encrypt. It also shows a list of all encrypted data for the current user. do_encrypt/2 will handle the encryption itself. It takes the user’s data, gets the user’s key from the session, encrypts the user’s data with the key, and saves the encrypted data to the database.

    def encrypt(conn, _params)do
      user = get_session(conn, :user)
      key = get_session(conn, :key)
      user_data = UserEncryption.Account.get_all_user_data(user.id)
      |> Enum.map(fn it ->
        %{hash: it.data_hash, data: nil, id: it.id}
      end)
      render conn, "encrypt.html", data: user_data
    end

    def do_encrypt(conn, %{"data"=>data})do
      alias UserEncryption.Security.Utils
      alias UserEncryption.Account

      user = get_session(conn, :user)
      key = get_session(conn, :key)
      data_hash = Utils.encrypt(key, data)
      Account.create_user_data(%{user_id: user.id, data_hash: data_hash})
      conn |> redirect(to: "/encrypt")
    end

Add get_all_user_data/1 — a function that returns all encrypted data for a user from the database — to lib/user_encryption/account/account.ex

    def get_all_user_data(user_id)do
      query = (from d in UserData, where: d.user_id ==^user_id)
      Repo.all(query)
    end

Add encrypt template file lib/user_encryption/templates/page/encrypt.html.eex The encrypt template file has two parts. A form at the top with a field for the user to enter data to encrypt. The other part is the list of all encrypted data for the current user. Each item has a decrypt button to decrypt the encrypted data.

    <div class="row marketing">

    <!-- Form -->
    <form name="register" method="post">
    <input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>">
    <textarea placeholder="Enter data to encrypt" name="data" class="field" style="height:100px; width: 300px;"></textarea>
    <input type="Submit" value="Encrypt" class="field" />
    </form>

    <br/>

    <!--List of encrypted data -->

    <%= for d <- @data do %>
    <p><strong>Hash: </strong><%= d.hash %></p>
    <p>
    <form method="post" action="/decrypt/<%= d.id %>">
    <input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>">
    <input type="Submit" value="Decrypt" class="field" />
    </form></p>
    <% end %>

    </div>

Decrypting data

Add function decrypt/2 to lib/user_encryption/controllers/page_controller.ex decrypt/2 takes in an id. Uses the id to retrieve user data from the database, then decrypt the data using the key in the current session. If decryption is successful, the plain data is returned along with the data hash. Otherwise, only the data hash is returned. If the current user doesn’t own the data with the given id, obviously the decryption will fail.

    def do_decrypt(conn, %{"id"=>id})do
      user = get_session(conn, :user)
      key = get_session(conn, :key)
      user_data = UserEncryption.Account.get_user_data!(id)
      alias UserEncryption.Security.Utils

      d = case Utils.decrypt(key, user_data.data_hash)do
        {:ok, data}->
         %{hash: user_data.data_hash, data: data}
        _ ->
         %{hash: user_data.data_hash, data: nil}
       end
       conn|> render("decrypt.html", data: d)
    end

Add template file lib/user_encryption/templates/page/decrypt.html.eex

<div class="row marketing">
  <p><strong>Hash: </strong><%= @data.hash %></p>
  <p><strong>Data: </strong><%= @data.data %></p>
</div>

Changing User Password

Add 2 functions — changepassword/2 and do_changepassword/2 to lib/user_encryption/controllers/page_controller.ex changepassword/2 — will display the form for the current user to change their password and do_changepassword/2 will handle changing user’s password. It takes in old password, new password, and new password confirmation, validates the user’s old password, and updates the password and the user’s key hash.

    def do_change_password(conn, %{"old_password"=>old_password,
       "new_password"=>new_password})do
       alias UserEncryption.Security.Utils

       user = get_session(conn, :user)
       case UserEncryption.Account.validate_user(user, old_password)do
          %{key: key}->
             %{key_hash: key_hash} = Utils.update_key_hash(old_password,
                  user.key_hash,  new_password)
              UserEncryption.Account.update_user(user,
                %{key_hash: key_hash, password: new_password})
              conn
              |>clear_session()
              |> redirect(to: "/login")
        end
    end

    def change_password(conn, _p)do
      render(conn, "changepassword.html")
    end

Add template file for changing passwords. lib/user_encryption/templates/page/changepassword.html.eex

<div class="row marketing">
  <form name="register" method="post">
    <input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>" />
    <input
      placeholder="Old Password"
      name="old_password"
      type="password"
      class="field"
    />
    <input
      placeholder="New Password"
      name="new_password"
      type="password"
      class="field"
    />
    <input
      placeholder="Confirm NewPassword"
      name="confirm_password"
      type="password"
      class="field"
    />
    <input type="Submit" value="Change Password" class="field" />
  </form>
</div>

Finally, update the app layout template with our route links lib/user_encryption/templates/layout.app.html.eex

<body>
  <div class="container">
    <header class="header">
      <nav role="navigation">
        <ul class="nav nav-pills pull-right">
          <li>
            <%= if Plug.Conn.get_session(@conn, :user) do %> Welcome, <%=
            Plug.Conn.get_session(@conn, :user) |> Map.get(:name) %> <% end %>
          </li>
        </ul>
      </nav>
      <span class="logo"></span>
    </header>
    <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
    <p class="alert alert-danger" role="alert">
      <%= get_flash(@conn, :error) %>
    </p>

    <%= if Plug.Conn.get_session(@conn, :user) do %>
    <a href="/encrypt">Encrypt</a>
    | <a href="/changepassword">Change Pwd</a> | <a href="/logout">Logout</a>
    <% end %>
    <main role="main">
      <%= render @view_module, @view_template, assigns %>
    </main>
  </div>
  <!-- /container -->
  <script src='<%= static_path(@conn, "/js/app.js") %>'></script>
</body>

Compile and run

    $ iex -S mix phx.server

If all goes well, you should be able to see the registration page at http://localhost:4000/register and you should be able to register, login, encrypt and decrypt user data.

Check out the project repo. Thanks for reading.

Happy coding!