defmodule Cryppo do
@moduledoc """
Main public API of Cryppo
"""
import Cryppo.Strategies, only: [find_strategy: 1, find_key_derivation_strategy: 1]
alias Cryppo.{
DerivedKey,
EncryptedData,
EncryptedDataWithDerivedKey,
EncryptionKey,
RsaSignature,
Serialization,
Strategies
}
@typedoc """
Name of an encryption or derivation strategy
Use `Cryppo.encryption_strategies/0` to get a list of encryption strategies.
Use `Cryppo.derivation_strategies/0` to get a list of derivation strategies.
"""
@type encryption_strategy() :: String.t()
@typedoc """
Module of an encryption or derivation strategy
"""
@type encryption_strategy_module() :: atom
@doc "List available encryption strategies"
@spec encryption_strategies :: [encryption_strategy()]
def encryption_strategies, do: Strategies.encryption_strategies()
@doc "List available derivation strategies"
@spec derivation_strategies :: [encryption_strategy()]
def derivation_strategies, do: Strategies.derivation_strategies()
@doc """
Generate an encryption key for an encryption strategy
The generated encrypted key is marked as belonging to the encryption strategy.
## Example
iex> _encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
"""
@spec generate_encryption_key(encryption_strategy) ::
EncryptionKey.t() | {:unsupported_encryption_strategy, binary}
def generate_encryption_key(encryption_strategy) when is_binary(encryption_strategy) do
with {:ok, mod} <- find_strategy(encryption_strategy) do
%EncryptionKey{} = apply(mod, :generate_key, [])
end
end
@doc """
Encrypt data with an encryption key
## Example
iex> encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
iex> _encrypted_data = Cryppo.encrypt("data to encrypt", "Aes256Gcm", encryption_key)
The encryption key must match the encryption strategy:
iex> encryption_key = Cryppo.generate_encryption_key("Aes256Gcm")
iex> Cryppo.encrypt("data to encrypt", "Rsa4096", encryption_key)
{:incompatible_key, [submitted_key_strategy: Cryppo.Aes256gcm, encryption_strategy: Cryppo.Rsa4096]}
"""
@spec encrypt(binary, encryption_strategy, EncryptionKey.t() | any) ::
EncryptedData.t()
| {:unsupported_encryption_strategy, atom}
| {:error, :invalid_encryption_key}
| :encryption_error
| {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
def encrypt(data, encryption_strategy, encryption_key_or_raw_key)
when is_binary(encryption_strategy) and is_binary(data) do
with {:ok, mod} <- find_strategy(encryption_strategy) do
encryption_key_or_raw_key =
encryption_key_or_raw_key
|> add_encryption_strategy_module(mod)
apply(mod, :run_encryption, [data, encryption_key_or_raw_key])
end
end
@doc """
Generate an encryption key for an encryption strategy and encrypt data with this encryption key
## Example
iex> {_encrypted_data, _encryption_key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
"""
@spec encrypt(binary, encryption_strategy) ::
EncryptedData.t()
| {:unsupported_encryption_strategy, atom}
| :encryption_error
| {:encryption_error, any}
def encrypt(data, encryption_strategy)
when is_binary(encryption_strategy) and is_binary(data) do
with {:ok, mod} <- find_strategy(encryption_strategy) do
encryption_key = apply(mod, :generate_key, [])
encrypted = apply(mod, :run_encryption, [data, encryption_key])
{encrypted, encryption_key}
end
end
defp add_encryption_strategy_module(key, mod) do
case key do
%EncryptionKey{encryption_strategy_module: nil} = key ->
%{key | encryption_strategy_module: mod}
k ->
k
end
end
@doc """
Decrypt encrypted data with an encryption key
## Example
iex> {encrypted_data, encryption_key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
iex> Cryppo.decrypt(encrypted_data, encryption_key)
{:ok, "data to encrypt"}
"""
@spec decrypt(EncryptedData.t(), EncryptionKey.t() | any) ::
{:ok, binary}
| {:error, :invalid_encryption_key}
| :decryption_error
| {:decryption_error, {any, any}}
| {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
def decrypt(
%EncryptedData{encryption_strategy_module: mod} = encrypted_data,
encryption_key_or_raw_key
) do
encryption_key_or_raw_key = encryption_key_or_raw_key |> add_encryption_strategy_module(mod)
apply(mod, :run_decryption, [encrypted_data, encryption_key_or_raw_key])
end
@doc """
Encrypt data with a derived key
## Example
iex> _encrypted = Cryppo.encrypt_with_derived_key("data to encrypt", "Aes256Gcm", "Pbkdf2Hmac", "passphrase")
"""
@spec encrypt_with_derived_key(binary, encryption_strategy(), encryption_strategy(), String.t()) ::
EncryptedDataWithDerivedKey.t()
| {:unsupported_encryption_strategy, encryption_strategy}
| {:unsupported_key_derivation_strategy, encryption_strategy}
def encrypt_with_derived_key(data, encryption_strategy, key_derivation_strategy, passphrase)
when is_binary(encryption_strategy) and is_binary(key_derivation_strategy) and
is_binary(passphrase) and is_binary(data) do
with {:ok, key_derivation_mod} <- find_key_derivation_strategy(key_derivation_strategy),
{:ok, encryption_strategy_mod} <- find_strategy(encryption_strategy) do
if apply(encryption_strategy_mod, :key_derivation_possible, []) do
key_length = apply(encryption_strategy_mod, :key_length, [])
%DerivedKey{encryption_key: key} =
derived_key = apply(key_derivation_mod, :generate_derived_key, [passphrase, key_length])
key_with_encryption_strategy = %{
key
| encryption_strategy_module: encryption_strategy_mod
}
%EncryptedData{} =
encrypted_data =
apply(encryption_strategy_mod, :run_encryption, [data, key_with_encryption_strategy])
%EncryptedDataWithDerivedKey{encrypted_data: encrypted_data, derived_key: derived_key}
else
{:encryption_strategy_does_not_support_key_derivation, encryption_strategy}
end
end
end
@doc """
Decrypt data with a derived key
## Example
iex> encrypted = Cryppo.encrypt_with_derived_key("data to encrypt", "Aes256Gcm", "Pbkdf2Hmac", "passphrase")
iex> {:ok, decrypted, _key} = Cryppo.decrypt_with_derived_key(encrypted, "passphrase")
iex> decrypted
"data to encrypt"
"""
@spec decrypt_with_derived_key(EncryptedDataWithDerivedKey.t(), String.t()) ::
{:ok, binary, DerivedKey.t()}
| :decryption_error
| {:decryption_error, {any, any}}
| {:incompatible_key, submitted_key_strategy: atom, encryption_strategy: atom}
def decrypt_with_derived_key(
%EncryptedDataWithDerivedKey{
derived_key: %DerivedKey{key_derivation_strategy: key_derivation_mod} = derived_key,
encrypted_data:
%EncryptedData{encryption_strategy_module: encryption_strategy_mod} = encrypted_data
},
passphrase
)
when is_binary(passphrase) do
derived_key =
%DerivedKey{encryption_key: key} =
apply(key_derivation_mod, :build_derived_key, [passphrase, derived_key])
key_with_encryption_strategy = %{key | encryption_strategy_module: encryption_strategy_mod}
with {:ok, decrypted} <-
apply(encryption_strategy_mod, :run_decryption, [
encrypted_data,
key_with_encryption_strategy
]) do
{:ok, decrypted, derived_key}
end
end
@doc """
Serialize various Cryppo data structures as a string
3 Cryppo data structures have their own serialization formats:
* `Cryppo.EncryptedData`
* `Cryppo.EncryptedDataWithDerivedKey`
* `Cryppo.RsaSignature`
## Examples
`Cryppo.EncryptedData`:
iex> {encrypted_data, _key} = Cryppo.encrypt("data to encrypt", "Aes256Gcm")
iex> Cryppo.serialize(encrypted_data)
`Cryppo.EncryptedDataWithDerivedKey`:
iex> "data to encrypt"
...> |> Cryppo.encrypt_with_derived_key("Aes256Gcm", "Pbkdf2Hmac", "passphrase")
...> |> Cryppo.serialize()
`Cryppo.RsaSignature`:
iex> private_key = Cryppo.generate_encryption_key("Rsa4096")
iex> "data to encrypt"
...> |> Cryppo.Rsa4096.sign(private_key)
...> |> Cryppo.serialize()
"""
@spec serialize(EncryptedData.t() | EncryptedDataWithDerivedKey.t() | RsaSignature.t()) ::
binary
def serialize(s)
def serialize(%EncryptedData{} = s), do: Serialization.serialize(s)
def serialize(%EncryptedDataWithDerivedKey{} = s), do: Serialization.serialize(s)
def serialize(%RsaSignature{} = s), do: Serialization.serialize(s)
@doc """
Load various Cryppo data structures from their serialized forms
3 Cryppo data structures have their own serialization formats:
* `Cryppo.EncryptedData`
* `Cryppo.EncryptedDataWithDerivedKey`
* `Cryppo.RsaSignature`
## Examples
iex> s = "Aes256Gcm.WSDb2AmsF7LFOxYb.QUAAAAACYWQABQAAAG5vbmUABWF0ABAAAAAAY9Ck6LzVGiMdiWFK6N5BawVpdgAMAAAAAG_Yxh-I0gGNYoFRigA="
iex> {:ok, %Cryppo.EncryptedData{}} = Cryppo.load(s)
iex> s = "Aes256Gcm.wW4M_sv_kMx14cC6.QUAAAAACYWQABQAAAG5vbmUABWF0ABAAAAAA8Aq84t28sMT9FL8cz-TmMQVpdgAMAAAAANahbwbkfWo18YuCMgA=.Pbkdf2Hmac.SzAAAAAQaQAJUwAABWl2ABQAAAAAuQTqZLVFO49lI6Kx454ffYQ9VV0QbAAgAAAAAA=="
iex> {:ok, %Cryppo.EncryptedDataWithDerivedKey{}} = Cryppo.load(s)
iex> s = "Sign.Rsa4096.V4JbRzpkud-3cHCGqDwGjS3TmRto5Te0iSAtD7oIzsDa83McBDYpU_eeswVZF9AGEvoAEQOCwpqJ_PgbjHKT2nHgLysK-btG6Nxk_K2J7A6Uq15X5QrOgIKTzC00dj1tzAN73u9lsRPKIfwPyp_Mlb6FNs1LoB7OvAusit6QPm8iAwHo4nOWBBUf3hO9b3gsWJ92FxnBsCLYFQj_zv4mnLHj7pDNVtq9Kp4hK6bgcIH4FZtyDKDr6bXEtlCGLDIY10UqNLylkagI36Gyafm-HnD57vRxjgHIGEsd2XcwDJ8PqqrzSYNxl-RyWD3wq0nXE_1rYJ7k1AKLM5G1Hg8B2whqcXpQ52x3zVFCAjlU9GNhT6pdUBxQYw09va7fe2w517PrwwMe90MW87fj3G7dGEKT95cDLTx1d84ybIUFUJOGKY0FF4LL0E3UqWQ92kU4bh-DSTkNmgItX34fiBIOpQDbF238IkRYyFA8LfMPfL-0_dnto9sH0E3Umi41qFvpA2Nq8r57FF4vCOSkXYWVfyitOkY_URqMLxS57azwZRBehJYDtvbqmzaYEDceeLjkxDi--Y10LT4Cz2SGiU--YDJM66PZ3Cp74gvDpsWlohcwYmMib5LrjdtvLOAtOZhoLZyGeeX0lDnwOum7lFRpJd8UIrOlTvpBo48ep2bpmgA=.VmVyaMO8dHVuZyB2ZXJib3Rlbg=="
iex> {:ok, %Cryppo.RsaSignature{}} = Cryppo.load(s)
"""
@spec load(binary) ::
{:ok, EncryptedDataWithDerivedKey.t() | EncryptedData.t() | RsaSignature.t()}
| {:error,
:invalid_base64
| :invalid_bson
| :invalid_derivation_artefacts
| :invalid_serialization_value
| :invalid_encryption_artefacts
| String.t()}
| {:unsupported_encryption_strategy, binary}
| {:unsupported_key_derivation_strategy, binary}
def load(serialized) when is_binary(serialized) do
case String.split(serialized, ".") do
["Sign", "Rsa4096", signature, data] ->
RsaSignature.load(signature, data)
[strategy_name, encrypted_data, encryption_artefacts] ->
EncryptedData.load(strategy_name, encrypted_data, encryption_artefacts)
[
strategy,
encrypted_data,
encryption_artefacts,
key_derivation_strategy,
derivation_artefacts
] ->
EncryptedDataWithDerivedKey.load(
strategy,
encrypted_data,
encryption_artefacts,
key_derivation_strategy,
derivation_artefacts
)
_ ->
{:error, :invalid_serialization_value}
end
end
end