Skip to content

Instantly share code, notes, and snippets.

@potatosalad
Last active October 14, 2016 16:44
Show Gist options
  • Select an option

  • Save potatosalad/2386bc90467a49385ba472acca565227 to your computer and use it in GitHub Desktop.

Select an option

Save potatosalad/2386bc90467a49385ba472acca565227 to your computer and use it in GitHub Desktop.
Example plug message encryptor using libsodium
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