Current section
Files
Jump to
Current section
Files
README.md
# Encryptor
[](https://github.com/riddler/encryptor/actions/workflows/ci.yml)
[](https://hex.pm/packages/encryptor)
[](https://hex.pm/packages/encryptor)
[](https://hexdocs.pm/encryptor/)
[](https://github.com/riddler/encryptor/blob/main/LICENSE)
Application-layer encryption for Elixir: a vault module your code calls,
pluggable key providers, per-scope keys in envelopes, and rotation. It runs on
the [aws_encryption_sdk](https://hex.pm/packages/aws_encryption_sdk) engine,
so every ciphertext is a standard AWS Encryption SDK message.
## Why encryptor
Encrypting data before it reaches the database usually means choosing between
a thin wrapper over `:crypto` that leaves every key question to you, and a full
encryption SDK client whose surface is shaped for the cryptography rather than
for your application. Either way the questions a real application asks stay
open: which key does this record use, where does the key material come from,
and how does a key rotate without a migration. With this package your call
sites name one vault module and nothing else; where key material comes from is
an adapter behind one behaviour, so the source can change without the call
sites changing; each scope you key by (an account, a workspace) gets its own
key, which a ciphertext names, so rotation is re-encryption against a new
version and a crypto-shred destroys one scope's key; and the messages stay in
the AWS format, readable from the official SDKs in other languages.
## Install
Add `encryptor` to the dependencies in your `mix.exs`:
```elixir
def deps do
[
{:encryptor, "~> 0.7.0"}
]
end
```
Raw-keyring use pulls in no AWS, HTTP or XML library; only the KMS-backed
providers bring that stack in. Two dependencies are optional:
`{:argon2_elixir, "~> 4.0"}` for `Encryptor.Kdf.slow_hash/3`, and
`{:goth, "~> 1.4"}` for `Encryptor.Provider.GcpKms`.
## Basic usage
A single-key vault encrypting one column. The key is 32 random bytes, Base64
in the environment, and it reaches the vault through `init/1` at start: a `use`
option such as `:key` fails compilation.
```elixir
defmodule MyApp.Vault do
use Encryptor.Vault,
otp_app: :my_app,
context_profile: :single,
required_context: ["table", "column"]
@impl true
def init(config) do
key = Base.decode64!(System.fetch_env!("MY_APP_VAULT_KEY"))
provider = {Encryptor.Provider.Static, key: key, namespace: "my_app", name: "data/v1"}
{:ok, Keyword.put(config, :provider, provider)}
end
end
# With MyApp.Vault in your supervision tree:
context = %{"table" => "notes", "column" => "body"}
{:ok, ciphertext} = MyApp.Vault.encrypt("a private note", encryption_context: context)
{:ok, "a private note"} = MyApp.Vault.decrypt(ciphertext, encryption_context: context)
# A write that leaves out a required context key is refused, not written unbound.
{:error, %Encryptor.Error{reason: {:missing_required_context_keys, ["column"]}}} =
MyApp.Vault.encrypt("a private note", encryption_context: %{"table" => "notes"})
# A decrypt under any other context fails, and every such failure looks the same.
{:error, %Encryptor.Error{reason: :decrypt_failed}} =
MyApp.Vault.decrypt(ciphertext, encryption_context: %{"table" => "t", "column" => "c"})
```
`ciphertext` is the whole self-describing message: you store that one binary,
and there is no second column to keep in step with it.
## Documentation
- Learn
- [Getting started](guides/getting-started.md): a single-key vault, then a scoped vault with one key per scope, and the two root secrets a deployment provisions on day one.
- Do
- [How to source secrets at start](guides/secrets-at-start.md): read key material from the environment or a secrets manager in `init/1`, and what each mistake looks like at start.
- [How to rotate, retire, shred and suspend keys](guides/rotation-runbook.md): the five operator procedures, what each step destroys, and the GCP operator section.
- [Encrypt Ecto schema columns](https://hexdocs.pm/encryptor_ecto): `encryptor_ecto`, the companion package with the Ecto types, the wrapped-key storage and the re-encryption migrator.
- Look up
- [The vault](https://hexdocs.pm/encryptor/Encryptor.Vault.html): the functions `use Encryptor.Vault` generates, the lifecycle checks, `derive/3`, `suspend/2` and `reinstate/2`.
- [Vault configuration](https://hexdocs.pm/encryptor/Encryptor.Vault.Config.html): the five-layer precedence chain, each option and what is checked at start, and choosing an algorithm suite.
- [Key providers](https://hexdocs.pm/encryptor/Encryptor.Provider.html): the behaviour, its conformance suite, and the `Static`, `Function`, `Kms` and `GcpKms` adapters.
- [Key derivation](https://hexdocs.pm/encryptor/Encryptor.Kdf.html): the HKDF trees, the label grammar, derived subkeys and the Argon2id slow hash.
- [The materials cache bound](https://hexdocs.pm/encryptor/Encryptor.Vault.CacheRecycler.html): how `:recycle_after` bounds the engine's cache, and why dropping the table is safe.
- [Telemetry events](https://hexdocs.pm/encryptor/Encryptor.Telemetry.html): the closed event set, its measurements and its allow-listed metadata.
- [Errors](https://hexdocs.pm/encryptor/Encryptor.Error.html): the one error struct and its closed reason vocabulary.
- [The changelog](https://github.com/riddler/encryptor/blob/main/CHANGELOG.md): what changed in each version, and what to do about each breaking change.
- Understand
- [The security model: keys, scopes and envelopes](docs/explanation/security-model.md): the three levels of keys, why a scope's key is random and stored rather than derived, what the encryption context binds, why decrypt failures look alike, and what the model does not protect against.
- [Choosing the scope](guides/choosing-the-scope.md): what a scope is, where to draw its boundary, the rotate, suspend and shred verbs, and what cryptographic erasure honestly achieves.
- [The decision records](https://github.com/riddler/encryptor/tree/main/docs/adr): the record behind every cryptographic choice here, with an index of what each one decides.
## Compatibility
The package needs Elixir 1.18 or later (`elixir: "~> 1.18"` in `mix.exs`). Its
runtime dependencies are `aws_encryption_sdk ~> 1.0` and `telemetry ~> 1.3`;
`argon2_elixir ~> 4.0` and `goth ~> 1.4` are optional. CI runs the full
gate on Erlang/OTP 27 and the test suite on Erlang/OTP 26, both with Elixir
1.18. Ciphertexts are interoperable with the official AWS Encryption SDKs:
data written from Elixir is readable from Java, Python, JavaScript or the AWS
CLI, and the other way round.
Two open engine issues are worked round here until they move:
[#95](https://github.com/riddler/aws-encryption-sdk-elixir/issues/95), an
unbounded materials cache, which the vault bounds by recycling it, and
[#96](https://github.com/riddler/aws-encryption-sdk-elixir/issues/96), a warm
decryption cache that skips context validation, which the vault replaces with
its own comparison.
Until 1.0, the public surface may change between minor releases: a release may
rename modules, callbacks, telemetry events or error vocabulary with no
compatibility shim. Every such change is recorded in the
[changelog](https://github.com/riddler/encryptor/blob/main/CHANGELOG.md) under
a bold **Breaking** heading that says what to do about it, and pinning to an
exact minor, `~> X.Y.0`, is the recommended way to take the package until
then. Do not depend on `encryptor 0.1.0`: it is a name reservation with no
code in it.
## License
Apache-2.0 - see
[LICENSE](https://github.com/riddler/encryptor/blob/main/LICENSE).