Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/cloak/padding.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Cloak.Padding do
def pad(str, size \\ 16) do
if byte_size(str) < size do
str
|> Kernel.<>("\x80")
|> String.pad_trailing(size - 1, "\x00")
else
str
Copy link
Owner

@danielberkompas danielberkompas Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we don't have any test coverage of this line. We should have a test that tests what happens if the string is already longer than the padding.

end
end

def unpad(str) do
String.replace(str, ~r/\x80[\x00]+$/, "")
end
end
114 changes: 89 additions & 25 deletions lib/cloak/vault.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,40 +121,53 @@ defmodule Cloak.Vault do
@type plaintext :: binary
@type ciphertext :: binary
@type label :: atom
@type opts :: Keyword.t()

@doc """
Encrypts a binary using the first configured cipher in the vault's
configured `:ciphers` list.
"""
@callback encrypt(plaintext) :: {:ok, ciphertext} | {:error, Exception.t()}

@callback encrypt(plaintext, opts) :: {:ok, ciphertext} | {:error, Exception.t()}

@doc """
Like `encrypt/1`, but raises any errors.
"""
@callback encrypt!(plaintext) :: ciphertext | no_return

@callback encrypt!(plaintext, opts) :: ciphertext | no_return

@doc """
Encrypts a binary using the vault's configured cipher with the
corresponding label.
"""
@callback encrypt(plaintext, label) :: {:ok, ciphertext} | {:error, Exception.t()}

@callback encrypt(plaintext, label, opts) :: {:ok, ciphertext} | {:error, Exception.t()}

@doc """
Like `encrypt/2`, but raises any errors.
"""
@callback encrypt!(plaintext, label) :: ciphertext | no_return

@callback encrypt!(plaintext, label, opts) :: ciphertext | no_return

@doc """
Decrypts a binary with the configured cipher that generated the binary.
Automatically detects which cipher to use, based on the ciphertext.
"""
@callback decrypt(ciphertext) :: {:ok, plaintext} | {:error, Exception.t()}

@callback decrypt(ciphertext, opts) :: {:ok, plaintext} | {:error, Exception.t()}

@doc """
Like `decrypt/1`, but raises any errors.
"""
@callback decrypt!(ciphertext) :: plaintext | no_return

@callback decrypt!(ciphertext, opts) :: plaintext | no_return

@doc """
The JSON library the vault uses to convert maps and lists into
JSON binaries before encryption.
Expand Down Expand Up @@ -220,44 +233,64 @@ defmodule Cloak.Vault do

@impl Cloak.Vault
def encrypt(plaintext) do
encrypt(plaintext, [])
end

@impl Cloak.Vault
def encrypt(plaintext, opts) when is_list(opts) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.encrypt(plaintext)
|> Cloak.Vault.encrypt(plaintext, opts)
end

@impl Cloak.Vault
def encrypt!(plaintext) do
encrypt!(plaintext, [])
end

@impl Cloak.Vault
def encrypt!(plaintext, opts) when is_list(opts) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.encrypt!(plaintext)
|> Cloak.Vault.encrypt!(plaintext, opts)
end

@impl Cloak.Vault
def encrypt(plaintext, label) do
def encrypt(plaintext, label, opts \\ []) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.encrypt(plaintext, label)
|> Cloak.Vault.encrypt(plaintext, label, opts)
end

@impl Cloak.Vault
def encrypt!(plaintext, label) do
def encrypt!(plaintext, label, opts \\ []) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.encrypt!(plaintext, label)
|> Cloak.Vault.encrypt!(plaintext, label, opts)
end

@impl Cloak.Vault
def decrypt(ciphertext) do
decrypt(ciphertext, [])
end

@impl Cloak.Vault
def decrypt(ciphertext, opts) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.decrypt(ciphertext)
|> Cloak.Vault.decrypt(ciphertext, opts)
end

@impl Cloak.Vault
def decrypt!(ciphertext) do
decrypt!(ciphertext, [])
end

@impl Cloak.Vault
def decrypt!(ciphertext, opts) do
@table_name
|> Cloak.Vault.read_config()
|> Cloak.Vault.decrypt!(ciphertext)
|> Cloak.Vault.decrypt!(ciphertext, opts)
end

@impl Cloak.Vault
Expand Down Expand Up @@ -292,18 +325,22 @@ defmodule Cloak.Vault do
end

@doc false
def encrypt(config, plaintext) do
with [{_label, {module, opts}} | _ciphers] <- config[:ciphers] do
module.encrypt(plaintext, opts)
def encrypt(config, plaintext, opts) do
padding = Keyword.get(opts, :padding, false)

with [{_label, {module, module_opts}} | _ciphers] <- config[:ciphers] do
plaintext
|> maybe_pad_plaintext(padding)
|> module.encrypt(module_opts)
else
_ ->
{:error, Cloak.InvalidConfig.exception("could not encrypt due to missing configuration")}
end
end

@doc false
def encrypt!(config, plaintext) do
case encrypt(config, plaintext) do
def encrypt!(config, plaintext, opts) do
case encrypt(config, plaintext, opts) do
{:ok, ciphertext} ->
ciphertext

Expand All @@ -313,19 +350,23 @@ defmodule Cloak.Vault do
end

@doc false
def encrypt(config, plaintext, label) do
def encrypt(config, plaintext, label, opts) do
padding = Keyword.get(opts, :padding, false)

case config[:ciphers][label] do
nil ->
{:error, Cloak.MissingCipher.exception(vault: config[:vault], label: label)}

{module, opts} ->
module.encrypt(plaintext, opts)
{module, module_opts} ->
plaintext
|> maybe_pad_plaintext(padding)
|> module.encrypt(module_opts)
end
end

@doc false
def encrypt!(config, plaintext, label) do
case encrypt(config, plaintext, label) do
def encrypt!(config, plaintext, label, opts) do
case encrypt(config, plaintext, label, opts) do
{:ok, ciphertext} ->
ciphertext

Expand All @@ -334,20 +375,35 @@ defmodule Cloak.Vault do
end
end

defp maybe_pad_plaintext(plaintext, false) do
plaintext
end

defp maybe_pad_plaintext(plaintext, size) when is_integer(size) do
Cloak.Padding.pad(plaintext, size)
end

defp maybe_pad_plaintext(plaintext, _padding) do
Cloak.Padding.pad(plaintext)
end

@doc false
def decrypt(config, ciphertext) do
def decrypt(config, ciphertext, opts) do
padding = Keyword.get(opts, :padding, false)

case find_module_to_decrypt(config, ciphertext) do
nil ->
{:error, Cloak.MissingCipher.exception(vault: config[:vault], ciphertext: ciphertext)}

{_label, {module, opts}} ->
module.decrypt(ciphertext, opts)
{_label, {module, module_opts}} ->
{:ok, maybe_padded_plaintext} = module.decrypt(ciphertext, module_opts)
maybe_unpad_ciphertext(maybe_padded_plaintext, padding)
end
end

@doc false
def decrypt!(config, ciphertext) do
case decrypt(config, ciphertext) do
def decrypt!(config, ciphertext, opts) do
case decrypt(config, ciphertext, opts) do
{:ok, plaintext} ->
plaintext

Expand All @@ -356,9 +412,17 @@ defmodule Cloak.Vault do
end
end

defp maybe_unpad_ciphertext(ciphertext, false) do
{:ok, ciphertext}
end

defp maybe_unpad_ciphertext(ciphertext, _padding) do
{:ok, Cloak.Padding.unpad(ciphertext)}
end

defp find_module_to_decrypt(config, ciphertext) do
Enum.find(config[:ciphers], fn {_label, {module, opts}} ->
module.can_decrypt?(ciphertext, opts)
Enum.find(config[:ciphers], fn {_label, {module, module_opts}} ->
module.can_decrypt?(ciphertext, module_opts)
end)
end
end
Loading