• Stars
    star
    274
  • Rank 150,274 (Top 3 %)
  • Language
    Elixir
  • Created almost 7 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐Ÿ” A detailed example for how to encrypt data in an Elixir (Phoenix v1.7) App before inserting into a database using Ecto Types

Phoenix Ecto Encryption Example

data encrypted

GitHub Workflow Status codecov.io Hex.pm docs contributions welcome HitCount


๐Ÿ’ก Note: we wrote this example/tutorial to understand how to do field-level encryption from first principals.
Once we solved the problem, we built a library to streamline it: fields.
We still recommend going through this example, but if you just want to get on with building your Phoenix App, use fields.


Why?

Encrypting User/Personal data stored by your Web App is essential for security/privacy.

If your app offers any personalised content or interaction that depends on "login", it is storing personal data (by definition). You might be tempted to think that data is "safe" in a database, but it's not. There is an entire ("dark") army/industry of people (cybercriminals) who target websites/apps attempting to "steal" data by compromising databases. All the time you spend building your app, they spend trying to "break" apps like yours. Don't let the people using your app be the victims of identity theft, protect their personal data! (it's both the "right" thing to do and the law ...)

What?

This example/tutorial is intended as a comprehensive answer to the question:

"How to Encrypt/Decrypt Sensitive Data in Elixir Apps Before Inserting (Saving) it into the Database?"

Technical Overview

We are not "re-inventing encryption" or using our "own algorithm" everyone knows that's a "bad idea": https://security.stackexchange.com/questions/18197/why-shouldnt-we-roll-our-own
We are following a battle-tested industry-standard approach and applying it to our Elixir/Phoenix App.
We are using:

ยฏ\_(ใƒ„)_/ยฏ...? Don't be "put off" if any of these terms/algorithms are unfamiliar to you;
this example is "step-by-step" and we are happy to answer/clarify any (relevant and specific) questions you have!

OWASP Cryptographic Rules?

This example/tutorial follows the Open Web Application Security Project (OWASP) Cryptographic and Password rules:

  • Use "strong approved Authenticated Encryption" based on an AES algorithm.
    • Use GCM mode of operation for symmetric key cryptographic block ciphers.
    • Keys used for encryption must be rotated at least annually.
  • Only use approved public algorithm SHA-256 or better for hashing.
  • Argon2 is the winner of the password hashing competition and should be your first choice for new applications.

See:

Who?

This example/tutorial is for any developer (or technical decision maker / "application architect")
who takes personal data protection seriously and wants a robust/reliable and "transparent" way
of encrypting data before storing it, and decrypting when it is queried.

Prerequisites?

If you are totally new to (or "rusty" on) Elixir, Phoenix or Ecto, we recommend going through our Phoenix Chat Example (Beginner's Tutorial) first: https://github.com/dwyl/phoenix-chat-example

Crypto Knowledge?

You will not need any "advanced" mathematical knowledge; we are not "inventing" our own encryption or going into the "internals" of any cyphers/algorithms/schemes.

You do not need to understand how the encryption/hashing algorithms work,
but it is useful to know the difference between encryption vs. hashing and plaintext vs. ciphertext.

The fact that the example/tutorial follows all OWASP crypto/hashing rules (see: "OWASP Cryptographic Rules?" section above), should be "enough" for most people who just want to focus on building their app and don't want to "go down the rabbit hole".

However ... We have included 30+ links in the "Useful Links" section at the end of this readme. The list includes several common questions (and answers) so if you are curious, you can learn.

Note: in the @dwyl Library we have https://www.schneier.com/books/applied_cryptography So, if you're really curious let us know!

Time Requirement?

Simply reading ("skimming") through this example will only take 15 minutes.
Following the examples on your computer (to fully understand it) will take around 1 hour
(including reading a few of the links).

Invest the time up-front to avoid on the embarrassment and fines of a data breach.

How?

These are "step-by-step" instructions, don't skip any step(s).

1. Create the encryption App

In your Terminal, create a new Phoenix application called "encryption":

mix phx.new encryption

When you see Fetch and install dependencies? [Yn],
type y and press the [Enter] key to download and install the dependencies.
You should see following in your terminal:

* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development

We are almost there! The following steps are missing:

    $ cd encryption

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server

Follow the first instruction change into the encryption directory:

cd encryption

Next create the database for the App using the command:

mix ecto.create

You should see the following output:

Compiling 13 files (.ex)
Generated encryption app
The database for Encryption.Repo has been created

2. Create the user Schema (Database Table)

In our example user database table, we are going to store 3 (primary) pieces of data.

  • name: the person's name (encrypted)
  • email: their email address (encrypted)
  • password_hash: the hashed password (so the person can login)

In addition to the 3 "primary" fields, we need one more field to store "metadata":

  • email_hash: so we can check ("lookup") if an email address is in the database without having to decrypt the email(s) stored in the DB.

Create the user schema using the following generator command:

mix phx.gen.schema User users email:binary email_hash:binary name:binary password_hash:binary

phx.gen.schema

The reason we are creating the encrypted/hashed fields as :binary is that the data stored in them will be encrypted and :binary is the most efficient Ecto/SQL data type for storing encrypted data; storing it as a String would take up more bytes for the same data. i.e. wasteful without any benefit to security or performance.
see: https://dba.stackexchange.com/questions/56934/what-is-the-best-way-to-store-a-lot-of-user-encrypted-data
and: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html

Next we need to update our newly created migration file. Open priv/repo/migrations/{timestamp}_create_users.exs.

Your migration file will have a slightly different name to ours as migration files are named with a timestamp when they are created but it will be in the same location.

Update the file from:

defmodule Encryption.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add(:email, :binary)
      add(:email_hash, :binary)
      add(:name, :binary)
      add(:password_hash, :binary)

      timestamps()
    end
  end
end

To

defmodule Encryption.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add(:email, :binary)
      add(:email_hash, :binary)
      add(:name, :binary)
      add(:password_hash, :binary)

      timestamps()
    end

    create(unique_index(:users, [:email_hash]))
  end
end

The newly added line ensures that we will never be allowed to enter duplicate email_hash values into our database.

Run the "migration" task to create the tables in the Database:

mix ecto.migrate

Running the mix ecto.migrate command will create the users table in your encryption_dev database.
You can view this (empty) table in a PostgreSQL GUI. Here is a screenshot from pgAdmin:
elixir-encryption-pgadmin-user-table

3. Define The 6 Functions

We need 6 functions for encrypting, decrypting, hashing and verifying the data we will be storing:

  1. Encrypt - to encrypt any personal data we want to store in the database.
  2. Decrypt - decrypt any data that needs to be viewed.
  3. Get Key - get the latest encryption/decryption key (or a specific older key where data was encrypted with a different key)
  4. Hash Email (deterministic & fast) - so that we can "lookup" an email without "decrypting". The hash of an email address should always be the same.
  5. Hash Password (pseudorandom & slow) - the output of the hash should always be different and relatively slow to compute.
  6. Verify Password - check a password against the stored password_hash to confirm that the person "logging-in" has the correct password.

The next 6 sections of the example/tutorial will walk through the creation of (and testing) these functions.

Note: If you have any questions on these functions, please ask:
github.com/dwyl/phoenix-ecto-encryption-example/issues

3.1 Encrypt

Create a file called lib/encryption/aes.ex and copy-paste (or hand-write) the following code:

defmodule Encryption.AES do
  @aad "AES256GCM" # Use AES 256 Bit Keys for Encryption.

  def encrypt(plaintext) do
    iv = :crypto.strong_rand_bytes(16) # create random Initialisation Vector
    key = get_key()    # get the *latest* key in the list of encryption keys
    {ciphertext, tag} =
      :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, to_string(plaintext), @aad, true)
    iv <> tag <> ciphertext # "return" iv with the cipher tag & ciphertext
  end

  defp get_key do # this is a "dummy function" we will update it in step 3.3
    <<109, 182, 30, 109, 203, 207, 35, 144, 228, 164, 106, 244, 38, 242,
    106, 19, 58, 59, 238, 69, 2, 20, 34, 252, 122, 232, 110, 145, 54,
    241, 65, 16>> # return a random 32 Byte / 128 bit binary to use as key.
  end
end

The encrypt/1 function for encrypting plaintext into ciphertext is quite simple; (the "body" is only 4 lines).

Let's "step through" these lines one at a time:

Having different ciphertext each time plaintext is encrypted is essential for "semantic security" whereby repeated use of the same encryption key and algorithm does not allow an "attacker" to infer relationships between segments of the encrypted message. Cryptanalysis techniques are well "beyond scope" for this example/tutorial, but we highly encourage to check-out the "Background Reading" links at the end and read up on the subject for deeper understanding.

  • Next we use the get_key/0 function to retrieve the latest encryption key so we can use it to encrypt the plaintext (the "real" get_key/0 is defined below in section 3.3).

  • Then we use the Erlang block_encrypt function to encrypt the plaintext.
    Using :aes_gcm ("Advanced Encryption Standard Galois Counter Mode"):

    • @aad is a "module attribute" (Elixir's equivalent of a "constant") is defined in aes.ex as @aad "AES256GCM"
      this simply defines the encryption mode we are using which, if you break down the code into 3 parts:
      • AES = Advanced Encryption Standard.
      • 256 = "256 Bit Key"
      • GCM = "Galois Counter Mode"
  • Finally we "return" the iv with the ciphertag & ciphertext, this is what we store in the database. Including the IV and ciphertag is essential for allowing decryption, without these two pieces of data, we would not be able to "reverse" the process.

Note: in addition to this encrypt/1 function, we have defined an encrypt/2 "sister" function which accepts a specific (encryption) key_id so that we can use the desired encryption key for encrypting a block of text. For the purposes of this example/tutorial, it's not strictly necessary, but it is included for "completeness".

Test the encrypt/1 Function

Create a file called test/lib/aes_test.exs and copy-paste the following code into it:

defmodule Encryption.AESTest do
  use ExUnit.Case
  alias Encryption.AES

  test ".encrypt includes the random IV in the value" do
    <<iv::binary-16, ciphertext::binary>> = AES.encrypt("hello")

    assert String.length(iv) != 0
    assert String.length(ciphertext) != 0
    assert is_binary(ciphertext)
  end

  test ".encrypt does not produce the same ciphertext twice" do
    assert AES.encrypt("hello") != AES.encrypt("hello")
  end
end

Run these two tests by running the following command:

mix test test/lib/aes_test.exs

The full function definitions for AES encrypt/1 & encrypt/2 are in: lib/encryption/aes.ex
And tests are in: test/lib/aes_test.exs

3.2 Decrypt

The decrypt function reverses the work done by encrypt; it accepts a "blob" of ciphertext (which as you may recall), has the IV and cypher tag prepended to it, and returns the original plaintext.

In the lib/encryption/aes.ex file, copy-paste (or hand-write) the following decrypt/1 function definition:

def decrypt(ciphertext) do
  <<iv::binary-16, tag::binary-16, ciphertext::binary>> =
    ciphertext
  :crypto.crypto_one_time_aead(:aes_256_gcm, get_key(), iv, ciphertext, @aad, tag, false)
end

The fist step (line) is to "split" the IV from the ciphertext using Elixir's binary pattern matching.

If you are unfamiliar with Elixir binary pattern matching syntax: <<iv::binary-16, tag::binary-16, ciphertext::binary>> read the following guide: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html

The :crypto.crypto_one_time_aead(:aes_256_gcm, get_key(key_id), iv, ciphertext, @aad, tag, false) line is the very similar to the encrypt function.

The ciphertext is decrypted using block_decrypt/4 passing in the following parameters:

  • :aes_256_gcm = encyrption algorithm
  • get_key(key_id) = get the encryption key used to encrypt the plaintext
  • iv = the original Initialisation Vector used to encrypt the plaintext
  • {@aad, ciphertext, tag} = a Tuple with the encryption "mode", ciphertext and the tag that was originally used to encrypt the ciphertext.

Finally return just the original plaintext.

Note: as above with the encrypt/2 function, we have defined an decrypt/2 "sister" function which accepts a specific (encryption) key_id so that we can use the desired encryption key for decrypting the ciphertext. For the purposes of this example/tutorial, it's not strictly necessary, but it is included for "completeness".

Test the decrypt/1 Function

In the test/lib/aes_test.exs add the following test:

test "decrypt/1 ciphertext that was encrypted with default key" do
  plaintext = "hello" |> AES.encrypt |> AES.decrypt()
  assert plaintext == "hello"
end

Re-run the tests mix test test/lib/aes_test.exs and confirm they pass.

The full encrypt & decrypt function definitions with @doc comments are in: lib/encryption/aes.ex


> And tests are in: [`test/lib/aes_test.exs`](https://github.com/dwyl/phoenix-ecto-encryption-example/blob/master/test/lib/aes_test.exs)

3.3 Key rotation

Key rotation is a "best practice" that limits the amount of data an "attacker" can decrypt if the database were ever "compromised" (provided we keep the encryption keys safe that is!) A really good guide to this is: https://cloud.google.com/kms/docs/key-rotation.

For this reason we want to 'store' a key_id. The key_id indicates which encryption key was used to encrypt the data. Besides the IV and ciphertag, the key_id is also essential for allowing decryption, so we change the encrypt/1 function to preserve the key_id as well

defmodule Encryption.AES do
  @aad "AES256GCM" # Use AES 256 Bit Keys for Encryption.

  def encrypt(plaintext) do
    iv = :crypto.strong_rand_bytes(16)
    # get latest key
    key = get_key()
    # get latest ID;
    key_id = get_key_id()
    # {ciphertext, tag} = :crypto.block_encrypt(:aes_gcm, key, iv, {@aad, plaintext, 16})
    {ciphertext, tag} = :crypto.block_encrypt(:aes_gcm, key, iv, {@aad, to_string(plaintext), 16})
    iv <> tag <> <<key_id::unsigned-big-integer-32>> <> ciphertext
  end

  defp get_key do
    get_key_id() |> get_key
  end

  defp get_key(key_id) do
    encryption_keys() |> Enum.at(key_id)
  end

  defp get_key_id do
    Enum.count(encryption_keys()) - 1
  end

  defp encryption_keys do
    Application.get_env(:encryption, Encryption.AES)[:keys]
  end
end

For the complete file containing these functions see: lib/encryption/aes.ex

For this example/demo we are using two encryption keys which are kept as an application environment variable. The values of the encryptions keys are associated with the key Encryption.AES. During the encryption we are by default always using the latest (most recent) encryption key (get_key/0) and the corresponding key_id is fetched by get_key_id/0 which becomes part of the ciphertext.

With decrypting we now pattern match the associated key_id from the ciphertext in order to be able to decrypt with the correct encryption key.

  def decrypt(ciphertext) do
    <<iv::binary-16, tag::binary-16, key_id::unsigned-big-integer-32, ciphertext::binary>> =
      ciphertext

    :crypto.block_decrypt(:aes_gcm, get_key(key_id), iv, {@aad, ciphertext, tag})
  end

So we defined the get_key twice in lib/encryption/aes.ex as per Erlang/Elixir standard, once for each "arity" or number of "arguments". In the first case get_key/0 assumes you want the latest Encryption Key. The second case get_key/1 lets you supply the key_id to be "looked up":

Both versions of get_key use encryption_keys/0 function to call the Application.get_env function: Application.get_env(:encryption, Encryption.AES)[:keys] specifically. For this to work we need to define the keys as an Environment Variable and make it available to our App in config.exs.

3.4 ENCRYPTION_KEYS Environment Variable

In order for our get_key/0 and get_key/1 functions to work, it needs to be able to "read" the encryption keys.

We need to "export" an Environment Variable containing a (comma-separated) list of (one or more) encryption key(s).

Copy-paste (and run) the following command in your terminal:

echo "export ENCRYPTION_KEYS='nMdayQpR0aoasLaq1g94FLba+A+wB44JLko47sVQXMg=,L+ZVX8iheoqgqb22mUpATmMDsvVGtafoAeb0KN5uWf0='" >> .env && echo ".env" >> .gitignore

For now, copy paste this command exactly as it is.
When you are deploying your own App, generate your own AES encryption key(s) see: How To Generate AES Encryption Keys? section below for how to do this.

Note: there are two encryption keys separated by a comma. This is to demonstrate that it's possible to use multiple keys.

We prefer to store our Encryption Keys as Environment Variables this is consistent with the "12 Factor App" best practice: https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology

Update the config/config.exs to load the environment variables from the .env file into the application. Add the following code your config file just above import_config "#{Mix.env()}.exs":

# run shell command to "source .env" to load the environment variables.
try do                                     # wrap in "try do"
  File.stream!("./.env")                   # in case .env file does not exist.
    |> Stream.map(&String.trim_trailing/1) # remove excess whitespace
    |> Enum.each(fn line -> line           # loop through each line
      |> String.replace("export ", "")     # remove "export" from line
      |> String.split("=", parts: 2)       # split on *first* "=" (equals sign)
      |> Enum.reduce(fn(value, key) ->     # stackoverflow.com/q/33055834/1148249
        System.put_env(key, value)         # set each environment variable
      end)
    end)
rescue
  _ -> IO.puts "no .env file found!"
end

# Set the Encryption Keys as an "Application Variable" accessible in aes.ex
config :encryption, Encryption.AES,
  keys: System.get_env("ENCRYPTION_KEYS") # get the ENCRYPTION_KEYS env variable
    |> String.replace("'", "")  # remove single-quotes around key list in .env
    |> String.split(",")        # split the CSV list of keys
    |> Enum.map(fn key -> :base64.decode(key) end) # decode the key.

Test the get_key/0 and get_key/1 Functions?

Given that get_key/0 and get_key/1 are both defp (i.e. "private") they are not "exported" with the AES module and therefore cannot be invoked outside of the AES module.

The get_key/0 and get_key/1 are invoked by encrypt/1 and decrypt/1 and thus provided these (public) latter functions are tested adequately, the "private" functions will be too.

Re-run the tests mix test test/lib/aes_test.exs and confirm they still pass.

We also define a test in order to verify the working of key rotation. We add a new encryption key and assert (and make sure) that an encrypted value with an older encryption key will still be decrypted correctly.

  test "can still decrypt the value after adding a new encryption key" do
    encrypted_value = "hello" |> AES.encrypt()

    original_keys = Application.get_env(:encryption, Encryption.AES)[:keys]

    # add a new key
    Application.put_env(:encryption, Encryption.AES,
      keys: original_keys ++ [:crypto.strong_rand_bytes(32)]
    )

    assert "hello" == encrypted_value |> AES.decrypt()

    # rollback to the original keys
    Application.put_env(:encryption, Encryption.AES, keys: original_keys)
  end

The full encrypt & decrypt function definitions with @doc comments are in: lib/encryption/aes.ex And tests are in: test/lib/aes_test.exs

4. Hash Email Address

The idea behind hashing email addresses is to allow us to perform a lookup (in the database) to check if the email has already been registered/used for app/system.

Imagine that [email protected] has previously used your app. The SHA256 hash (encoded as base64) is: "bbYebcvPI5DkpGr0JvJqEzo77kUCFCL8euhukTbxQRA="

try it for yourself in iex:

iex(1)> email = "[email protected]"
"[email protected]"
iex(2)> email_hash = :crypto.hash(:sha256, email) |> Base.encode64
"bbYebcvPI5DkpGr0JvJqEzo77kUCFCL8euhukTbxQRA="

If we store the email_hash in the database, when Alex wants to log-in to the App/System, we simply perform a "lookup" in the users table:

hash  = :crypto.hash(:sha256, email) |> Base.encode64
query = "SELECT * FROM users WHERE email_hash = $1"
user  = Ecto.Adapters.SQL.query!(Encryption.Repo, query, [hash])

Note: there's a "built-in" Ecto get_by function to perform this type of
"SELECT ... WHERE field = value" query effortlessly

4.1 Generate the SECRET_KEY_BASE

All Phoenix apps have a secret_key_base for sessions. see: https://hexdocs.pm/plug/1.13.6/Plug.Session.COOKIE.html

Run the following command to generate a new phoenix secret key:

mix phx.gen.secret

copy-paste the output (64bit String) into your .env file after the "equals sign" on the line for SECRET_KEY_BASE:

export SECRET_KEY_BASE={YourSecreteKeyBaseGeneratedUsing-mix_phx.gen.secret}

Your .env file should look similar to: .env_sample

Load the secret key into your environment by typing into your terminal:

source .env

Note: We are using an .env file, but if you are using a "Cloud Platform" to deploy your app,
you could consider using their "Key Management Service" for managing encryption keys. eg
:

We now need to update our config files again. Open your config.exs file and change the the following: from

  secret_key_base: "3PXN/6k6qoxqQjWFskGew4r74yp7oJ1UNF6wjvJSHjC5Y5LLIrDpWxrJ84UBphJn",
  # your secret_key_base will be different but that is fine.

To

  secret_key_base: System.get_env("SECRET_KEY_BASE"),

As mentioned above, all Phoenix applications come with a secret_key_base. Instead of using this default one, we have told our application to use the new one that we added to our .env file.

Now we need to edit our config/test.exs file. Change the following: from

config :encryption, EncryptionWeb.Endpoint,
  http: [port: 4001],
  server: false

To

config :encryption, EncryptionWeb.Endpoint,
  http: [port: 4001],
  server: false,
  secret_key_base: System.get_env("SECRET_KEY_BASE")

By adding the previous code block we will now have a secret_key_base which we will be able to use for testing.

5. Create and use HashField Custom Ecto Type

When we first created the Ecto Schema for our "user", in Step 2 (above) This created the lib/encryption/user.ex file with the following schema:

schema "users" do
  field :email, :binary
  field :email_hash, :binary
  field :name, :binary
  field :password_hash, :binary

  timestamps()
end

The default Ecto field types (:binary) are a good start. But we can do so much better if we define custom Ecto Types!

Ecto Custom Types are a way of automatically "pre-processing" data before inserting it into (and reading from) a database. Examples of "pre-processing" include:

  • Custom Validation e.g: phone number or address format.
  • Encrypting / Decrypting
  • Hashing

A custom type expects 6 callback functions to be implemented in the file:

  • type/0 - define the Ecto Type we want Ecto to use to store the data for our Custom Type. e.g: :integer or :binary
  • cast/1 - "typecasts" (converts) the given data to the desired type e.g: Integer to String.
  • dump/1 - performs the "processing" on the raw data before it get's "dumped" into the Ecto Native Type.
  • load/1 - called when loading data from the database and receive an Ecto native type.
  • embed_as/1 - the return value (:self or :dump) determines how the type is treated inside embeds (not used here).
  • equal?/2 - invoked to determine if changing a type's field value changes the corresponding database record.

Create a file called lib/encryption/hash_field.ex and add the following:

defmodule Encryption.HashField do
  @behaviour Ecto.Type

  def type, do: :binary

  def cast(value) do
    {:ok, to_string(value)}
  end

  def dump(value) do
    {:ok, hash(value)}
  end

  def load(value) do
    {:ok, value}
  end

  def embed_as(_), do: :self

  def equal?(value1, value2), do: value1 == value2

  def hash(value) do
    :crypto.hash(:sha256, value <> get_salt(value))
  end

  # Get/use Phoenix secret_key_base as "salt" for one-way hashing Email address
  # use the *value* to create a *unique* "salt" for each value that is hashed:
  defp get_salt(value) do
    secret_key_base =
      Application.get_env(:encryption, EncryptionWeb.Endpoint)[:secret_key_base]
    :crypto.hash(:sha256, value <> secret_key_base)
  end
end

Let's step through each of these

type/0

The best data type for storing encrypted data is :binary (it uses half the "space" of a :string for the same ciphertext).

cast/1

Cast any data type to_string before encrypting it. (the encrypted data "ciphertext" will be of :binary type)

dump/1

The hash/1 function use Erlang's crypto library hash/2 function.

  • First we tell the hash/2 function that we want to use :sha256 "SHA 256" is the most widely used/recommended hash; it's both fast and "secure".
  • We then hash the value passed in to the hash/1 function (we defined) and concatenate it with "salt" using the get_salt/1 function which retrieves the secret_key_base environment variable and computes a unique "salt" using the value.

We use the SHA256 one-way hash for speed. We "salt" the email address so that the hash has some level of "obfuscation", in case the DB is ever "compromised" the "attacker" still has to "compute" a "rainbow table" from scratch.

load/1

Return the hash value as it is read from the database.

embed_as/1

This callback is only of importance when the type is part of an embed. It's not used here, but required for modules adopting the Ecto.Type behaviour as of Ecto 3.2.

equal?/2

This callback is invoked when we cast changes into a changeset and want to determine whether the database record needs to be updated. We use a simple equality comparison (==) to compare the current value to the requested update. If both values are equal, there's no need to update the record.

Note: Don't forget to export your SECRET_KEY_BASE environment variable (see instructions above)

The full file containing these two functions is: lib/encryption/hash_field.ex
And the tests for the functions are: test/lib/hash_field_test.exs

First add the alias for HashField near the top of the lib/encryption/user.ex file. e.g:

alias Encryption.HashField

Next, in the lib/encryption/user.ex file, update the lines for email_hash in the users schema
from:

schema "users" do
  field :email, :binary
  field :email_hash, :binary
  field :name, :binary
  field :password_hash, :binary
  timestamps()
end

To:

schema "users" do
  field :email, :binary
  field :email_hash, HashField
  field :name, :binary
  field :password_hash, :binary

  timestamps()
end
  def changeset(%User{} = user, attrs \\ %{}) do
    user
    |> cast(attrs, [:name, :email])
    |> validate_required([:email])
    |> add_email_hash
    |> unique_constraint(:email_hash)
  end

  defp add_email_hash(changeset) do
    if Map.has_key?(changeset.changes, :email) do
      changeset |> put_change(:email_hash, changeset.changes.email)
    else
      changeset
    end
  end

We should test this new functionality. Create the file test/lib/user_test.exs and add the following:

defmodule Encryption.UserTest do
  use Encryption.DataCase
  alias Encryption.User

  @valid_attrs %{
    name: "Max",
    email: "[email protected]",
    password: "NoCarbsBeforeMarbs"
  }

  @invalid_attrs %{}

  describe "Verify correct working of hashing" do
    setup do
      user = Repo.insert!(User.changeset(%User{}, @valid_attrs))
      {:ok, user: user, email: @valid_attrs.email}
    end

    test "inserting a user sets the :email_hash field", %{user: user} do
      assert user.email_hash == user.email
    end

    test ":email_hash field is the encrypted hash of the email", %{user: user} do
      user_from_db = User |> Repo.one()
      assert user_from_db.email_hash == Encryption.HashField.hash(user.email)
    end
  end
end

For the full user tests please see: test/user/user_test.exs

6. Create and user Hash Password Custom Ecto type

When hashing passwords, we want to use the strongest hashing algorithm and we also want the hashed value (or "digest") to be different each time the same plaintext is hashed (unlike when hashing the email address where we want a deterministic digest).

Using argon2 makes "cracking" a password (in the event of the database being "compromised") far less likely as it uses both a CPU-bound "work-factor" and a "Memory-hard" algorithm which will significantly "slow down" the attacker.

Add the argon2 Dependency

In order to use argon2 we must add it to our mix.exs file: in the defp deps do (dependencies) section, add the following line:

{:argon2_elixir, "~> 1.3"},  # securely hashing & verifying passwords

You will need to run mix deps.get to install the dependency.

6.1 Define the hash_password/1 Function

Create a file called lib/encryption/password_field.ex in your project. The first function we need is hash_password/1:

defmodule Encryption.PasswordField do

  def hash_password(value) do
    Argon2.Base.hash_password(to_string(value),
      Argon2.Base.gen_salt(), [{:argon2_type, 2}])
  end

end

hash_password/1 accepts a password to be hashed and invokes Argon2.Base.hash_password/3 passing in 3 arguments:

6.1.1 Test the hash_password/1 Function?

In order to test the PasswordField.hash_password/1 function we use the Argon2.verify_pass function to verify a password hash.

Create a file called test/lib/password_field_test.exs and copy-paste (or hand-type) the following test:

defmodule Encryption.PasswordFieldTest do
  use ExUnit.Case
  alias Encryption.PasswordField, as: Field

  test ".verify_password checks the password against the Argon2id Hash" do
    password = "EverythingisAwesome"
    hash = Field.hash_password(password)
    verified = Argon2.verify_pass(password, hash)
    assert verified
  end

end

Run the test using the command:

mix test test/lib/password_field_test.exs

The test should pass; if not, please re-trace the steps.

6.2 Verify Password

The corresponding function to check (or "verify") the password is verify_password/2. We need to supply both the password and stored_hash (the hash that was previously stored in the database when the person registered or updated their password) It then runs Argon2.verify_pass which does the checking.

def verify_password(password, stored_hash) do
  Argon2.verify_pass(password, stored_hash)
end

hash_password/1 and verify_password/2 functions are defined in: lib/encryption/password_field.ex

Test for verify_password/2

To test that our verify_password/2 function works as expected, open the file: test/lib/password_field_test.exs
and add the following code to it:

test ".verify_password fails if password does NOT match hash" do
  password = "EverythingisAwesome"
  hash = Field.hash_password(password)
  verified = Field.verify_password("LordBusiness", hash)
  assert !verified
end

Run the tests: mix test test/lib/password_field_test.exs and confirm they pass.

If you get stuck, see: test/lib/password_field_test.exs

Define the other Ecto.Type behaviour functions:

defmodule Encryption.PasswordField do
  @behaviour Ecto.Type

  def type, do: :binary

  def cast(value) do
    {:ok, to_string(value)}
  end

  def dump(value) do
    {:ok, hash_password(value)}
  end

  def load(value) do
    {:ok, value}
  end

  def embed_as(_), do: :self

  def equal?(value1, value2), do: value1 == value2

  def hash_password(value) do
    Argon2.Base.hash_password(to_string(value),
      Argon2.Base.gen_salt(), [{:argon2_type, 2}])
  end

  def verify_password(password, stored_hash) do
    Argon2.verify_pass(password, stored_hash)
  end
end
alias Encryption.{HashField, PasswordField, User}

Update the lines for :email and :name in the schema
from:

schema "users" do
  field :email, :binary
  field :email_hash, HashField
  field :name, :binary
  field :password_hash, :binary

  timestamps()
end

To:

schema "users" do
  field :email, :binary
  field :email_hash, HashField
  field :name, :binary
  field :password_hash, PasswordField

  timestamps()
end

7. Create and use EncryptedField Custom Ecto Type

Create a file called lib/encryption/encrypted_field.ex and add the following:

defmodule Encryption.EncryptedField do
  alias Encryption.AES  # alias our AES encrypt & decrypt functions (3.1 & 3.2)

  @behaviour Ecto.Type  # Check this module conforms to Ecto.type behavior.
  def type, do: :binary # :binary is the data type ecto uses internally

  # cast/1 simply calls to_string on the value and returns a "success" tuple
  def cast(value) do
    {:ok, to_string(value)}
  end

  # dump/1 is called when the field value is about to be written to the database
  def dump(value) do
    ciphertext = value |> to_string |> AES.encrypt
    {:ok, ciphertext} # ciphertext is :binary data
  end

  # load/1 is called when the field is loaded from the database
  def load(value) do
    {:ok, AES.decrypt(value)} # decrypted data is :string type.
  end

  # embed_as/1 dictates how the type behaves when embedded (:self or :dump)
  def embed_as(_), do: :self # preserve the type's higher level representation

  # equal?/2 is called to determine if two field values are semantically equal
  def equal?(value1, value2), do: value1 == value2
end

Let's step through each of these

type/0

The best data type for storing encrypted data is :binary (it uses half the "space" of a :string for the same ciphertext).

cast/1

Cast any data type to_string before encrypting it. (the encrypted data "ciphertext" will be of :binary type)

dump/1

Calls the AES.encrypt/1 function we defined in section 3.1 (above) so data is encrypted 'automatically' before we insert into the database.

load/1

Calls the AES.decrypt/1 function so data is 'automatically' decrypted when it is read from the database.

Note: the load/2 function is not required for Ecto Type compliance. Further reading: https://hexdocs.pm/ecto/Ecto.Type.html

embed_as/1

This callback is only of importance when the type is part of an embed. It's not used here, but required for modules adopting the Ecto.Type behaviour as of Ecto 3.2.

equal?/2

This callback is invoked when we cast changes into a changeset and want to determine whether the database record needs to be updated. We use a simple equality comparison (==) to compare the current value to the requested update. If both values are equal, there's no need to update the record.

Your encrypted_field.ex Custom Ecto Type should look like this: lib/encryption/encrypted_field.ex try to write the tests for the callback functions, if you get "stuck", take a look at: test/lib/encrypted_field_test.exs

Now that we have defined a Custom Ecto Type EncryptedField, we can use the Type in our User Schema. Add the following line to "alias" the Type and a User in the lib/encryption/user.ex file:

alias Encryption.{HashField, PasswordField, EncryptedField, User}

Update the lines for :email and :name in the schema
from:

schema "users" do
  field :email, :binary
  field :email_hash, HashField
  field :name, :binary
  field :password_hash, PasswordField

  timestamps()
end

To:

schema "users" do
  field :email, EncryptedField
  field :email_hash, HashField
  field :name, EncryptedField
  field :password_hash, PasswordField

  timestamps()
end

8. Ensure All Tests Pass

Typically we will create git commit (if we don't already have one) for the "known state" where the tests were passing (before starting the refactor).

The commit before refactoring the example is: https://github.com/dwyl/phoenix-ecto-encryption-example/tree/3659399ec32ca4f07f45d0552b9cf25c359a2456

The corresponding Travis-CI build for this commit is: https://travis-ci.org/dwyl/phoenix-ecto-encryption-example/jobs/379887597#L833

Note: if you are new to Travis-CI see: https://github.com/dwyl/learn-travis

Conclusion

We have gone through how to create custom Ecto Types in order to define our own functions for handling (transforming) specific types of data.

Our hope is that you have understood the flow.

We plan to extend this tutorial include User Interface please "star" the repo if you would find that useful.



How To Generate AES Encryption Keys?

Encryption keys should be the appropriate length (in bits) as required by the chosen algorithm.

An AES 128-bit key can be expressed as a hexadecimal string with 32 characters.
It will require 24 characters in base64.

An AES 256-bit key can be expressed as a hexadecimal string with 64 characters.
It will require 44 characters in base64.

see: https://security.stackexchange.com/a/45334/117318

Open iex in your Terminal and paste the following line (then press enter)

:crypto.strong_rand_bytes(32) |> :base64.encode

You should see terminal output similar to the following:

elixir-generate-encryption-key

We generated 3 keys for demonstration purposes:

  • "h6pUk0ZccS0pYsibHZZ4Cd+PRO339rMA7sMz7FnmcGs="
  • "nMd/yQpR0aoasLaq1g94FL/a+A+wB44JLko47sVQXMg="
  • "L+ZVX8iheoqgqb22mUpATmMDsvVGt/foAe/0KN5uWf0="

These two Erlang functions are described in:

Base64 encoding the bytes generated by strong_rand_bytes will make the output human-readable (whereas bytes are less user-friendly).



Useful Links, FAQ & Background Reading

Understanding Advanced Encryption Standard (AES)

If you prefer to read, Ryo Nakao wrote an excellent post on understanding how AES encryption works: https://nakabonne.dev/posts/understanding-how-aes-encryption-works/

If you have the bandwidth and prefer a video, Computerphile (YouTube channel) has an great explaination:

aes-explanation

Running a Single Test

To run a single test (e.g: while debugging), use the following syntax:

mix test test/user/user_test.exs:9

For more detail, please see: https://hexdocs.pm/phoenix/testing.html

Ecto Validation Error format

When Ecto changeset validation fails, for example if there is a "unique" constraint on email address (so that people cannot re-register with the same email address twice), Ecto returns the changeset with an errors key:

#Ecto.Changeset<
  action: :insert,
  changes: %{
    email: <<224, 124, 228, 125, 105, 102, 38, 170, 15, 199, 228, 198, 245, 189,
      82, 193, 164, 14, 182, 8, 189, 19, 231, 49, 80, 223, 84, 143, 232, 92, 96,
      156, 100, 4, 7, 162, 26, 2, 121, 32, 187, 65, 254, 50, 253, 101, 202>>,
    email_hash: <<21, 173, 0, 16, 69, 67, 184, 120, 1, 57, 56, 254, 167, 254,
      154, 78, 221, 136, 159, 193, 162, 130, 220, 43, 126, 49, 176, 236, 140,
      131, 133, 130>>,
    key_id: 1,
    name: <<2, 215, 188, 71, 109, 131, 60, 147, 219, 168, 106, 157, 224, 120,
      49, 224, 225, 181, 245, 237, 23, 68, 102, 133, 85, 62, 22, 166, 105, 51,
      239, 198, 107, 247, 32>>,
    password_hash: <<132, 220, 9, 85, 60, 135, 183, 155, 214, 215, 156, 180,
      205, 103, 189, 137, 81, 201, 37, 214, 154, 204, 185, 253, 144, 74, 222,
      80, 158, 33, 173, 254>>
  },
  errors: [email_hash: {"has already been taken", []}],
  data: #Encryption.User<>,
  valid?: false
>

The errors part is:

[email_hash: {"has already been taken", []}]

A tuple wrapped in a keyword list.

Why this construct? A changeset can have multiple errors, so they're stored as a keyword list, where the key is the field, and the value is the error tuple. The first item in the tuple is the error message, and the second is another keyword list, with additional information that we would use when mapping over the errors in order to make them more user-friendly (though here, it's empty). See the Ecto docs for add_error/4 and traverse_errors/2 for more information.

So to access the error message "has already been taken" we need some pattern-matching and list popping:

{:error, changeset} = Repo.insert User.changeset(%User{}, @valid_attrs)
{:ok, message} = Keyword.fetch(changeset.errors, :email_hash)
msg = List.first(Tuple.to_list(message))
assert "has already been taken" == msg

To see this in action run:

mix test test/user/user_test.exs:40

Stuck / Need Help?

If you get "stuck", please open an issue on GitHub: https://github.com/nelsonic/phoenix-ecto-encryption-example/issues describing the issue you are facing with as much detail as you can.



Credits

Inspiration/credit/thanks for this example goes to Daniel Berkompas @danielberkompas for his post:
https://blog.danielberkompas.com/2015/07/03/encrypting-data-with-ecto

Daniel's post is for Phoenix v0.14.0 which is quite "old" now ...
therefore a few changes/updates are required.
e.g: There are no more "Models" in Phoenix 1.3 or Ecto callbacks.

Also his post only includes the "sample code" and is not a complete example
and does not explain the functions & Custom Ecto Types.
Which means anyone following the post needs to manually copy-paste the code ... and "figure out" the "gaps" themselves to make it work.
We prefer to include the complete "end state" of any tutorial (not just "samples")
so that anyone can git clone and run the code locally to fully understand it.

Still, props to Daniel for his post, a good intro to the topic!

More Repositories

1

english-words

๐Ÿ“ A text file containing 479k English words for all your dictionary/word-based projects e.g: auto-completion / autosuggestion
Python
9,337
star
2

learn-json-web-tokens

๐Ÿ” Learn how to use JSON Web Token (JWT) to secure your next Web App! (Tutorial/Example with Tests!!)
JavaScript
4,178
star
3

learn-to-send-email-via-google-script-html-no-server

๐Ÿ“ง An Example of using an HTML form (e.g: "Contact Us" on a website) to send Email without a Backend Server (using a Google Script) perfect for static websites that need to collect data.
HTML
3,140
star
4

repo-badges

โญ Use repo badges (build passing, coverage, etc) in your readme/markdown file to signal code quality in a project.
HTML
2,831
star
5

learn-tdd

โœ… A brief introduction to Test Driven Development (TDD) in JavaScript (Complete Beginner's Step-by-Step Tutorial)
JavaScript
2,698
star
6

start-here

๐Ÿ’ก A Quick-start Guide for People who want to dwyl โค๏ธ โœ…
1,734
star
7

learn-elixir

๐Ÿ’ง Learn the Elixir programming language to build functional, fast, scalable and maintainable web applications!
Elixir
1,611
star
8

learn-travis

๐Ÿ˜Ž A quick Travis CI (Continuous Integration) Tutorial for Node.js developers
JavaScript
1,251
star
9

Javascript-the-Good-Parts-notes

๐Ÿ“– Notes on the seminal "JavaScript the Good Parts: by Douglas Crockford
1,193
star
10

aws-sdk-mock

๐ŸŒˆ AWSomocks for Javascript/Node.js aws-sdk tested, documented & maintained. Contributions welcome!
TypeScript
1,113
star
11

learn-aws-lambda

โœจ Learn how to use AWS Lambda to easily create infinitely scalable web services
JavaScript
1,035
star
12

book

๐Ÿ“— Our Book on Full-Stack Web Application Development covering User Experience (UX) Design, Mobile/Offline/Security First, Progressive Enhancement, Continuous Integration/Deployment, Testing (UX/TDD/BDD), Performance-Driven-Development and much more!
Rust
818
star
13

hapi-auth-jwt2

๐Ÿ”’ Secure Hapi.js authentication plugin using JSON Web Tokens (JWT) in Headers, URL or Cookies
JavaScript
795
star
14

learn-hapi

โ˜€๏ธ Learn to use Hapi.js (Node.js) web framework to build scalable apps in less time
HTML
794
star
15

phoenix-chat-example

๐Ÿ’ฌ The Step-by-Step Beginners Tutorial for Building, Testing & Deploying a Chat app in Phoenix 1.7 [Latest] ๐Ÿš€
Elixir
760
star
16

learn-tachyons

๐Ÿ˜ Learn how to use Tachyons to craft beautiful, responsive and fast UI with functional CSS!
HTML
673
star
17

learn-phoenix-framework

๐Ÿ”ฅ Phoenix is the web framework without compromise on speed, reliability or maintainability! Don't settle for less. ๐Ÿš€
Elixir
650
star
18

javascript-todo-list-tutorial

โœ… A step-by-step complete beginner example/tutorial for building a Todo List App (TodoMVC) from scratch in JavaScript following Test Driven Development (TDD) best practice. ๐ŸŒฑ
JavaScript
623
star
19

learn-nightwatch

๐ŸŒœ Learn how to use Nightwatch.js to easily & automatically test your web apps in *real* web browsers.
JavaScript
585
star
20

learn-elm

๐ŸŒˆ discover the beautiful programming language that makes front-end web apps a joy to build and maintain!
HTML
483
star
21

learn-redux

๐Ÿ’ฅ Comprehensive Notes for Learning (how to use) Redux to manage state in your Web/Mobile (React.js) Apps.
HTML
446
star
22

hits

๐Ÿ“ˆ General purpose hits (page views) counter
Elixir
432
star
23

learn-devops

๐Ÿšง Learn the craft of "DevOps" (Developer Operations) to Deploy your App and Monitor it so it stays "Up"!
Shell
413
star
24

phoenix-liveview-counter-tutorial

๐Ÿคฏ beginners tutorial building a real time counter in Phoenix 1.7.14 + LiveView 1.0 โšก๏ธ Learn the fundamentals from first principals so you can make something amazing! ๐Ÿš€
Elixir
374
star
25

hapi-socketio-redis-chat-example

๐Ÿ’ฌ Real-time Chat using Hapi.js + Socket.io + Redis Pub/Sub (example with tests!!)
Elm
366
star
26

hapi-typescript-example

โšก Hapi.Js + Typescript = Awesomeness
TypeScript
351
star
27

learn-istanbul

๐Ÿ Learn how to use the Istanbul JavaScript Code Coverage Tool
JavaScript
339
star
28

learn-redis

๐Ÿ“• Need to store/access your data as fast as possible? Learn Redis! Beginners Tutorial using Node.js ๐Ÿš€
JavaScript
291
star
29

technology-stack

๐Ÿš€ Detailed description + diagram of the Open Source Technology Stack we use for dwyl projects.
JavaScript
288
star
30

learn-elasticsearch

๐Ÿ” Learn how to use ElasticSearch to power a great search experience for your project/product/website.
Elixir
265
star
31

elixir-auth-google

๐Ÿ‘คMinimalist Google OAuth Authentication for Elixir Apps. Tested, Documented & Maintained. Setup in 5 mins. ๐Ÿš€
Elixir
263
star
32

home

๐Ÿก ๐Ÿ‘ฉโ€๐Ÿ’ป ๐Ÿ’ก home is where you can [learn to] build the future surrounded by like-minded creative, friendly and [intrinsically] motivated people focussed on health, fitness and making things people and the world need!
246
star
33

learn-docker

๐Ÿšข Learn how to use docker.io containers to consistently deploy your apps on any infrastructure.
Dockerfile
220
star
34

learn-elm-architecture-in-javascript

๐Ÿฆ„ Learn how to build web apps using the Elm Architecture in "vanilla" JavaScript (step-by-step TDD tutorial)!
JavaScript
211
star
35

learn-environment-variables

๐Ÿ“Learn how to use Environment Variables to keep your passwords and API keys secret. ๐Ÿ”
JavaScript
201
star
36

learn-postgresql

๐Ÿ˜ Learn how to use PostgreSQL and Structured Query Language (SQL) to store and query your relational data. ๐Ÿ”
JavaScript
195
star
37

learn-tape

โœ… Learn how to use Tape for JavaScript/Node.js Test Driven Development (TDD) - Ten-Minute Testing Tutorial
JavaScript
187
star
38

sendemail

๐Ÿ’Œ Simplifies reliably sending emails from your node.js apps using AWS Simple Email Service (SES)
JavaScript
181
star
39

phoenix-todo-list-tutorial

โœ… Complete beginners tutorial building a todo list from scratch in Phoenix 1.7 (latest)
Elixir
171
star
40

quotes

๐Ÿ’ฌ a curated list of quotes that inspire action + code that returns quotes by tag/author/etc. ๐Ÿ’ก
Elixir
163
star
41

learn-heroku

๐Ÿ Learn how to deploy your web application to Heroku from scratch step-by-step in 7 minutes!
Python
153
star
42

decache

:shipit: Delete Cached node_modules useful when you need to "un-require" during testing for a fresh state.
JavaScript
151
star
43

learn-chrome-extensions

๐ŸŒ Discover how to build and deploy a Google Chrome Extension for your Project!
139
star
44

labels

๐Ÿท Sync GitHub Labels from any Source to Target Repositories for Consistency across all your projects!
Elixir
138
star
45

learn-ab-and-multivariate-testing

๐Ÿ†Ž Tutorial on A/B and multivariate testing โœ”๏ธ
137
star
46

ISO-27001-2013-information-technology-security

๐Ÿ” Probably the most boring-but-necessary repo on GitHub. If you care about the security/privacy of your data...! โœ…
136
star
47

auth

๐Ÿšช ๐Ÿ” UX-focussed Turnkey Authentication Solution for Web Apps/APIs (Documented, Tested & Maintained)
Elixir
135
star
48

web-form-to-google-sheet

A simple example of sending data from an ordinary web form straight to a Google Spreadsheet without a server.
HTML
133
star
49

app

Clear your mind. Organise your life. Ignore distractions. Focus on what matters.
Dart
133
star
50

phoenix-liveview-chat-example

๐Ÿ’ฌ Step-by-step tutorial creates a Chat App using Phoenix LiveView including Presence, Authentication and Style with Tailwind CSS
Elixir
130
star
51

learn-circleci

โœ… A quick intro to Circle CI (Continuous Integration) for JavaScript developers.
121
star
52

learn-regex

โ‰๏ธ A simple REGular EXpression tutorial in JavaScript
120
star
53

learn-react

"The possibilities are numerous once we decide to act and not react." ~ George Bernard Shaw
HTML
108
star
54

learn-aws-iot

๐Ÿ’ก Learn how to use Amazon Web Services Internet of Things (IoT) service to build connected applications.
JavaScript
101
star
55

env2

๐Ÿ’ป Simple environment variable (from config file) loader for your node.js app
JavaScript
100
star
56

how-to-choose-a-database

How to choose the right dabase
97
star
57

flutter-todo-list-tutorial

โœ… A detailed example/tutorial building a cross-platform Todo List App using Flutter ๐Ÿฆ‹
Dart
91
star
58

imgup

๐ŸŒ… Effortless image uploads to AWS S3 with automatic resizing including REST API.
Elixir
88
star
59

mvp

๐Ÿ“ฒ simplest version of the @dwyl app
Elixir
87
star
60

contributing

๐Ÿ“‹ Guidelines & Workflow for people contributing to our project(s) on GitHub. Please โญ to confirm you've read & understood! โœ…
86
star
61

learn-flutter

๐Ÿฆ‹ Learn how to use Flutter to Build Cross-platform Native Mobile Apps
JavaScript
86
star
62

javascript-best-practice

A collection of JavaScript Best Practices
83
star
63

learn-amazon-web-services

โญ Amazing Guide to using Amazon Web Services (AWS)! โ˜๏ธ
83
star
64

range-touch

๐Ÿ“ฑ Use HTML5 range input on touch devices (iPhone, iPad & Android) without bloatware!
JavaScript
83
star
65

learn-pre-commit

โœ… Pre-commit hooks let you run checks before allowing a commit (e.g. JSLint or check Test Coverage).
JavaScript
80
star
66

product-owner-guide

๐Ÿš€ A rough guide for people working with dwyl as Product Owners
78
star
67

phoenix-ecto-append-only-log-example

๐Ÿ“ A step-by-step example/tutorial showing how to build a Phoenix (Elixir) App where all data is immutable (append only). Precursor to Blockchain, IPFS or Solid!
Elixir
78
star
68

fields

๐ŸŒป fields is a collection of useful field definitions (Custom Ecto Types) that helps you easily define an Ecto Schema with validation, encryption and hashing functions so that you can ship your Elixir/Phoenix App much faster!
Elixir
77
star
69

goodparts

๐Ÿ™ˆ An ESLint Style that only allows JavaScript the Good Parts (and "Better Parts") in your code.
JavaScript
77
star
70

hapi-error

โ˜” Intercept errors in your Hapi Web App/API and send a *useful* message to the client OR redirect to the desired endpoint.
JavaScript
76
star
71

process-handbook

๐Ÿ“— Contains our processes, questions and journey to creating a team
HTML
76
star
72

terminate

โ™ป๏ธ Terminate a Node.js Process (and all Child Processes) based on the Process ID
JavaScript
76
star
73

aws-lambda-deploy

โ˜๏ธ ๐Ÿš€ Effortlessly deploy Amazon Web Services Lambda function(s) with a single command. Less to configure. Latest AWS SDK and Node.js v20!
JavaScript
74
star
74

dev-setup

โœˆ๏ธ A quick-start guide for new engineers on how to set up their Dev environment
73
star
75

hapi-login-example-postgres

๐Ÿฐ A simple registration + login form example using hapi-register, hapi-login & hapi-auth-jwt2 with a PostgreSQL DB
JavaScript
69
star
76

flutter-bloc-tutorial

A step-by-step example/tutorial explaining the benefits of the BLoC architecture and bloc library including tests!
Dart
67
star
77

phoenix-liveview-todo-list-tutorial

โœ… Beginners tutorial building a Realtime Todo List in Phoenix 1.6.10 + LiveView 0.17.10 โšก๏ธ Feedback very welcome!
Elixir
65
star
78

learn-security

๐Ÿ” For most technology projects Security is an "after thought", it does not have to be that way; let's be proactive!
64
star
79

learn-javascript

A Series of Simple Steps in JavaScript :-)
HTML
63
star
80

chat

๐Ÿ’ฌ Probably the fastest, most reliable/scalable chat system on the internet.
Elixir
62
star
81

learn-jsdoc

๐Ÿ“˜ Use JSDoc and a few carefully crafted comments to document your JavaScript code!
CSS
60
star
82

ampl

๐Ÿ“ฑ โšก Ampl transforms Markdown into AMP-compliant html so it loads super-fast!
JavaScript
58
star
83

aguid

โ„๏ธ A Globally Unique IDentifier (GUID) generator in JS. (deterministic or random - you chose!)
JavaScript
57
star
84

tudo

โœ… Want to see where you could help on an open dwyl issue?
Elixir
57
star
85

learn-apple-watch-development

๐Ÿ“— Learn how to build Native Apple Watch (+iPhone) apps from scratch!
Swift
55
star
86

learn-qunit

โœ… A quick introduction to JavaScript unit testing with QUnit
JavaScript
51
star
87

learn-ngrok

โ˜๏ธ Learn how to use ngrok to share access to a Web App/Site running on your "localhost" with the world!
HTML
50
star
88

learn-jshint

๐Ÿ’ฉ Learn how to use the ~~jshint~~ code quality/consistency tool.
JavaScript
49
star
89

hapi-auth-jwt2-example

๐Ÿ”’ A functional example Hapi.js app using hapi-auth-jwt2 & Redis (hosted on Heroku) with tests!
JavaScript
49
star
90

tachyons-bootstrap

๐Ÿ‘ขBootstrap recreated using tachyons functional css
HTML
48
star
91

learn-tailwind

๐ŸŒฌ๏ธ Learn Tailwind CSS to craft pixel-perfect web apps/sites in less time! ๐Ÿ˜
Elixir
48
star
92

esta

๐Ÿ” Simple + Fast ElasticSearch Node.js client. Beginner-friendly defaults & Heroku support โœ… ๐Ÿš€
JavaScript
48
star
93

learn-node-js-by-example

โ˜๏ธ Practical node.js examples.
HTML
47
star
94

elixir-pre-commit

โœ… Pre-commit hooks for Elixir projects
Elixir
46
star
95

product-roadmap

๐ŸŒ Because why wouldn't you make your company's product roadmap Public on GitHub?
46
star
96

redis-connection

โšก Single Redis Connection that can be used anywhere in your node.js app and closed once (e.g in tests)
JavaScript
45
star
97

learn-graphQL

โ“Learn to use GraphQL - A query language that allows client applications to specify their data fetching requirements
JavaScript
45
star
98

aws-lambda-test-utils

Mock event and context objects without fluff.
JavaScript
44
star
99

hapi-login

๐Ÿšช The Simplest Possible (Email + Password) Login for Hapi.js Apps โœ…
JavaScript
43
star
100

learn-riot

๐ŸŽ Riot.js lets you build apps that are simpler and load/run faster than any other JS framework/library.
HTML
43
star