Last active
October 14, 2016 16:44
-
-
Save potatosalad/2386bc90467a49385ba472acca565227 to your computer and use it in GitHub Desktop.
Example plug message encryptor using libsodium
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule Sodium.MessageEncryptor do | |
| @moduledoc ~S""" | |
| AES256-GCM Content-Encryption with AES256-GCM Key-Wrapping using [libsodium](https://github.com/potatosalad/erlang-libsodium). | |
| For example: | |
| # Encryption: `secret' must be at least 256-bits, `sign_secret' may be any length | |
| iex> encrypted = Sodium.MessageEncryptor.encrypt("test", "abcdefghijklmnopqrstuvwxyz012345", "abcdefghijklmnop") | |
| "QTI1NkdDTQ.VGbRU_MS0G_sFvfpUkoXmK8Hl1TBI5tPVN_SUl6NI7yYrZDgdV3u2OrnWwXAgg8A-FZNOtJV7P-nICoQ.Ht5ty7EvPx-YY-bn.98CpxA.1KZaBS79yPKdXuTbdqL66A" | |
| # Decryption: `secret' must be at least 256-bits, `sign_secret' may be any length | |
| iex> {:ok, decrypted} = Sodium.MessageEncryptor.decrypt(encrypted, "abcdefghijklmnopqrstuvwxyz012345", "abcdefghijklmnop") | |
| "test" | |
| # Decryption: if `encrypted', `secret', or `sign_secret' are incorrect, :error is returned | |
| iex> Sodium.MessageEncryptor.decrypt(encrypted <> "bad", "abcdefghijklmnopqrstuvwxyz012345", "abcdefghijklmnop") | |
| :error | |
| """ | |
| @doc """ | |
| Encrypts a message using authenticated encryption. | |
| """ | |
| def encrypt(message, secret, sign_secret) | |
| when is_binary(message) and is_binary(secret) and is_binary(sign_secret) do | |
| aes256_gcm_encrypt(message, secret, sign_secret) | |
| end | |
| @doc """ | |
| Decrypts a message using authenticated encryption. | |
| """ | |
| def decrypt(encrypted, secret, sign_secret) | |
| when is_binary(encrypted) and is_binary(secret) and is_binary(sign_secret) do | |
| aes256_gcm_decrypt(encrypted, secret, sign_secret) | |
| end | |
| ## Authenticated Encryption Algorithms | |
| # Encrypts and authenticates a message using AES256-GCM mode. | |
| # | |
| # A random 256-bit content encryption key (CEK) is generated for | |
| # every message which is then encrypted with `aes_gcm_key_wrap/3`. | |
| defp aes256_gcm_encrypt(plain_text, secret, sign_secret) | |
| when bit_size(secret) > 256, | |
| do: aes256_gcm_encrypt(plain_text, binary_part(secret, 0, 32), sign_secret) | |
| defp aes256_gcm_encrypt(plain_text, secret, sign_secret) | |
| when is_binary(plain_text) | |
| and bit_size(secret) in [256] | |
| and is_binary(sign_secret) do | |
| key = :libsodium_randombytes.buf(:libsodium_crypto_aead_aes256gcm.keybytes()) | |
| iv = :libsodium_randombytes.buf(:libsodium_crypto_aead_aes256gcm.npubbytes()) | |
| aad = "A256GCM" | |
| {cipher_text, cipher_tag} = :libsodium_crypto_aead_aes256gcm.encrypt_detached(plain_text, aad, iv, key) | |
| encrypted_key = aes_gcm_key_wrap(key, secret, sign_secret) | |
| encode_token(aad, encrypted_key, iv, cipher_text, cipher_tag) | |
| end | |
| # Verifies and decrypts a message using AES256-GCM mode. | |
| # | |
| # Decryption will never be performed prior to verification. | |
| # | |
| # The encrypted content encryption key (CEK) is decrypted | |
| # with `aes_gcm_key_unwrap/3`. | |
| defp aes256_gcm_decrypt(cipher_text, secret, sign_secret) | |
| when bit_size(secret) > 256, | |
| do: aes256_gcm_decrypt(cipher_text, binary_part(secret, 0, 32), sign_secret) | |
| defp aes256_gcm_decrypt(cipher_text, secret, sign_secret) | |
| when is_binary(cipher_text) | |
| and bit_size(secret) in [256] | |
| and is_binary(sign_secret) do | |
| case decode_token(cipher_text) do | |
| {aad = "A256GCM", encrypted_key, iv, cipher_text, cipher_tag} | |
| when bit_size(iv) === 96 and bit_size(cipher_tag) === 128 -> | |
| encrypted_key | |
| |> aes_gcm_key_unwrap(secret, sign_secret) | |
| |> case do | |
| {:ok, key} -> | |
| :libsodium_crypto_aead_aes256gcm.decrypt_detached(cipher_text, cipher_tag, aad, iv, key) | |
| _ -> | |
| :error | |
| end | |
| |> case do | |
| plain_text when is_binary(plain_text) -> | |
| {:ok, plain_text} | |
| _ -> | |
| :error | |
| end | |
| _ -> | |
| :error | |
| end | |
| end | |
| # Wraps a decrypted content encryption key (CEK) with secret and | |
| # sign_secret using AES GCM mode. | |
| # | |
| # See: https://tools.ietf.org/html/rfc7518#section-4.7 | |
| defp aes_gcm_key_wrap(cek, secret, sign_secret) | |
| when bit_size(secret) > 256, | |
| do: aes_gcm_key_wrap(cek, binary_part(secret, 0, 32), sign_secret) | |
| defp aes_gcm_key_wrap(cek, secret, sign_secret) | |
| when bit_size(cek) in [256] | |
| and bit_size(secret) in [256] | |
| and is_binary(sign_secret) do | |
| iv = :libsodium_randombytes.buf(:libsodium_crypto_aead_aes256gcm.npubbytes()) | |
| {cipher_text, cipher_tag} = :libsodium_crypto_aead_aes256gcm.encrypt_detached(cek, sign_secret, iv, secret) | |
| cipher_text <> cipher_tag <> iv | |
| end | |
| # Unwraps an encrypted content encryption key (CEK) with secret and | |
| # sign_secret using AES GCM mode. | |
| # | |
| # See: https://tools.ietf.org/html/rfc7518#section-4.7 | |
| defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret) | |
| when bit_size(secret) > 256, | |
| do: aes_gcm_key_unwrap(wrapped_cek, binary_part(secret, 0, 32), sign_secret) | |
| defp aes_gcm_key_unwrap(wrapped_cek, secret, sign_secret) | |
| when bit_size(secret) in [256] | |
| and is_binary(sign_secret) do | |
| wrapped_cek | |
| |> case do | |
| << cipher_text :: 256-bitstring, cipher_tag :: 128-bitstring, iv :: 96-bitstring >> -> | |
| :libsodium_crypto_aead_aes256gcm.decrypt_detached(cipher_text, cipher_tag, sign_secret, iv, secret) | |
| _ -> | |
| :error | |
| end | |
| |> case do | |
| cek when bit_size(cek) in [256] -> | |
| {:ok, cek} | |
| _ -> | |
| :error | |
| end | |
| end | |
| ## Helpers | |
| defp encode_token(protected, encrypted_key, iv, cipher_text, cipher_tag) do | |
| protected | |
| |> Base.url_encode64(padding: false) | |
| |> Kernel.<>(".") | |
| |> Kernel.<>(Base.url_encode64(encrypted_key, padding: false)) | |
| |> Kernel.<>(".") | |
| |> Kernel.<>(Base.url_encode64(iv, padding: false)) | |
| |> Kernel.<>(".") | |
| |> Kernel.<>(Base.url_encode64(cipher_text, padding: false)) | |
| |> Kernel.<>(".") | |
| |> Kernel.<>(Base.url_encode64(cipher_tag, padding: false)) | |
| end | |
| defp decode_token(token) do | |
| case String.split(token, ".", parts: 5) do | |
| [protected, encrypted_key, iv, cipher_text, cipher_tag] -> | |
| # TODO: Use with/else once we depend on Elixir v1.3+ only | |
| with {:ok, protected} <- Base.url_decode64(protected, padding: false), | |
| {:ok, encrypted_key} <- Base.url_decode64(encrypted_key, padding: false), | |
| {:ok, iv} <- Base.url_decode64(iv, padding: false), | |
| {:ok, cipher_text} <- Base.url_decode64(cipher_text, padding: false), | |
| {:ok, cipher_tag} <- Base.url_decode64(cipher_tag, padding: false) do | |
| {true, protected, encrypted_key, iv, cipher_text, cipher_tag} | |
| end | |
| |> case do | |
| {true, protected, encrypted_key, iv, cipher_text, cipher_tag} -> | |
| {protected, encrypted_key, iv, cipher_text, cipher_tag} | |
| _ -> | |
| :error | |
| end | |
| _ -> | |
| :error | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment