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
Downloaded the latest release from https://download.libsodium.org/libsodium/releases/LATEST.tar.gz
Extracted the zip file and navigate to the directory containing the extracted files
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!