Per-User Encryption with Elixir IV
Creating the Elixir Project
In the previous post, we discussed the design of how to share and update encrypted documents with multiple users. In this post, we will create an Elixir project based on those ideas.
Create Elixir Project
Let’s start by creating a new elixir project using mix new
command
$ mix new user_encryption
This will create a new elixir project for us.
Project Dependencies
Since we are working with security stuff, we will need a cryptographic library. We will useenacl
(pronounced Na-Cl). The library provides Erlang bindings for the libsodium cryptographic library which we will use for generating public-private keys, perform encryption and decryption; both symmetric and asymmetric.
Update project dependencies in mix.exs
Add Common Cryptographic Function
Let’s add a module for our common crypto functions inlib/utils.ex
This module provides the needed cryptographic operations for our application as well as some private functions. The module contains functions for generating public-private key pairs (generate_key_pairs/0
), encrypting data (encrypt/1
, encrypt_message_for_user_with_pk/2
) and decrypting data (decrypt/1
, decrypt_message_for_user/2
)
Add User Module
Let’s add User
module lib/user.ex
to model data on users.
The user struct contains information about a user. username
, key_hash
and key_pair
. The key_hash is a password derived key that unique to the user.
The module also contains new/2
a helper function generating new user struct from username and password. The key_pair
field in the user struct contains a public key
and a private key hash
— which is a private key encrypted with the password derived key for that user. This is important since the private key for a user must be kept a secret. In this case, only that user can decrypt the private key. Note that, we don’t store the password of the user since we are storing the key_hash and we can verify a user’s password by trying to decrypt the key_hash with the password, say, during login.
Add Document and User Document Module
Add a module for modeling an encrypted document. First, the EncryptedDocument
module defines a struct with field id and data_hash — the encrypted content of the document — to represent data needed about our encrypted document. It also contains new/2
(new/3
); a helper function for creating a new encrypted document with a key and content (and optional document id). The content will be encrypted with the given key. Note that we do not store the key used to encrypt the document content in the document. That is handled in the UserDocument
module.
The UserDocument
module defines a struct that links a user to a document. To simplify things, the struct has two fields: id and user_key. The id
field is a concatenation of the user’s username
and the document’s id
. Note that you can keep separate fields for a user_id
and document_id
in the UserDocument
struct. This way we can list easily list all documents for a user and all users that have access to a document. The user_key
contains the document key encrypted with the user’s public key.
The module also contains new/3
; a function which takes in a user, an encrypted document, and a document key. The function returns a user document with the user_key
set to the document key encrypted with the user’s public key.
Add Database Module
Create a new file lib/database.ex
for our Database
module. This will be the main interface for our application— creating and verifying users and creating, updating, and sharing encrypted user documents.
The module contains a struct with fields — users
, documents
and user_documents
— an in-memory structure that holds all the data relevant to running our app. This can easily be represented in a real database as tables.
The users
field is a map of username as key and user struct as value; documents
field is a map of document id as key and document struct as value. Similarly, the user_documents
field is a map with the user-document id as key, and the user-document struct as the value.
The relevant functions in the module include the following:
User APIsadd_user/2
— takes in a database struct and a user — a map containing username and password. This function creates a new user and adds the user to the database.
login_user/2
— takes in a database struct and a user — a map containing username and password. This function verifies the user. The implementation basically checks if the encrypted password-derived user key can be decrypted with the given password.
get_user/2
— takes in a database and username and returns a user with that username (if found) or nil if no user is found with the given username.
Document APIsadd_document/4
takes in a database, a user, the user’s password, and content to create a new encrypted document for that user. If successful, the function returns the document created and the updated database — {:ok, created_document, updated_database}
otherwise, an error is returned.
decrypt_document/4
— takes in the database, a user, a document and a password to decrypt an encrypted document. The user must have access to the document and the password must be the correct password for that user. If successful, the decrypted content of the document is returned.
update_document/5
— takes in the database, a user, the document to update, the user’s password and the new content to update the document with. The user must have access to the document and the user’s password must be correct.
share_user_document/5
— takes in the database, the user who is sharing (user A), the user who the document is being shared with (user B), the encrypted document, and the sharer’s password. User A must have access to the document and the password must be correct. First, the function decrypts the encrypted document key for user A using the user A’s password. Once we have the decrypted document key, a user document record is created for user B, using user B’s public key. Next time user B accesses the document they can decrypt the encrypted document key with their private key and successfully access the content of the document.
Running and the code
$ iex -S mix
This should start the Elixir shell with our project loaded. Now let’s test our user API functions
iex(1)> alias UserEncrypted.Database
iex(2)> db = Database.new()iex(3)> username = "jose"
iex(4)> password = "12345"iex(5)> {:ok, db} = Database.add_user(db, %{username: username, password: password})iex(6)> :ok = Database.login_user(db, %{username: username, password: password})iex(7)> %{username: ^username} = Database.get_user(db, username)
Let’s test our document API functions
We will create an encrypted document for a user and decrypt it
iex(8)> user = Database.get_user(db, username)
iex(9)> content = "Hello world!"iex(10)> {:ok, doc, db} = Database.add_document(db, user, password,content)
iex(11)> {:ok, "Hello world!"} = Database.decrypt_document(db, user, doc,password)iex(12)> {:ok, doc, db} = Database.update_document(doc, user, password, "New Hello world!")
iex(13)> {:ok, "New Hello world!"} = Database.decrypt_document(db, user, doc, password)
Let’s test sharing the encrypted document with other users
iex(14)> {:ok, db} = Database.add_user(db, %{username: "joe", password: "1234567"})
iex(15)> joe = Database.get_user(db, "joe")iex(16)> {:ok, db} = Database.share_document(db, jose, joe, doc, password)
The new user can decrypt the document with their password
iex(17)> {:ok, "New Hello world!"} = Database.decrypt_document(db, user, doc, "1234567")
You can find the full repo code here on Github.
Happy coding!