Ergonomic envelope encryption for Elixir - a vault module, pluggable key providers, and per-tenant keys - on the aws_encryption_sdk engine.
Status: designed, not built
The five founding architecture decision records were accepted on
2026-08-27. They fix the contracts this package is made of. No
implementation has landed yet: lib/ holds a moduledoc, the package is not
published, and nothing below is a promise about a function you can call today.
| Record | Decides |
|---|---|
| ADR-0001 | The vault layer: one host-owned module that wraps the engine completely, what it supervises, how it is configured, how its cache is bounded, and its error vocabulary |
| ADR-0002 | The key-provider behaviour: a provider resolves a selector to a key descriptor, and only the vault turns a descriptor into a keyring |
| ADR-0003 | The per-tenant envelope: a tenant key is 32 random bytes wrapped into an ordinary message, and the host stores the wrapping |
| ADR-0004 | The encryption-context convention: the canonical keys, who supplies each, and how a vault enforces them |
| ADR-0005 | Rotation and crypto-shred: three independent lifecycles, four operator procedures, and the one step that cannot be undone |
The implementation graph derived from them is
docs/plans/260827-enc-2y3-b3-implementation-graph.md.
Work is tracked in this repository's own beads database, under the epic
"Implement the vault core".
Read the records before writing code here. Until a contract is fixed by an accepted record, it is open - and a cryptographic choice made inline in an implementation is a defect even when the choice happens to be a good one, because the record is what makes it reviewable.
The charter
Application-level encryption in Elixir usually arrives as one of two things: a
thin wrapper over :crypto that leaves key management to the caller, or a
full ESDK client whose surface is shaped for the cryptography rather than for
the application. Neither answers the questions a real application asks - which
key does this tenant's data use, how does that key rotate without a migration,
where does the key material actually come from. This package is the layer that
answers them:
A vault module.
use Encryptor.Vault, otp_app: :my_appgives a supervised client, configured from app config, withencrypt/decryptentry points a call site can use without naming a keyring, a client, or a cryptographic materials manager. One place to configure, one surface to call, and consumers never type the engine's namespace.Pluggable key providers. Where key material comes from is a behaviour, not a hard-coded choice. A static key resolved at boot, a wrapped key column in the database, a KMS call - each is an adapter behind the same contract, so the call sites do not change when the source does.
Per-tenant keys and rotation. A multi-tenant host app needs each tenant's data encrypted under that tenant's own key, and needs to roll that key on its own schedule. Key identity and key version are first-class: a ciphertext records which key encrypted it, decryption resolves the key it names, and rotation is re-encryption against a new version rather than a flag day. Because a tenant key is random rather than derived, destroying its wrapping destroys the key, so a crypto-shred is honest.
AWS Encryption SDK message format - ciphertexts are interoperable with the official ESDKs, so data written from Elixir is readable from Java, Python, JavaScript, or the AWS CLI, and vice versa.
The engine stays aws_encryption_sdk. Raw-keyring usage pulls no AWS, HTTP,
or XML libraries - that client stack is optional in the engine, and only
KMS-backed providers will bring it in.
The family
| Package | Owns |
|---|---|
encryptor (here) | The vault surface, the key-provider behaviour, the envelope and key-derivation scheme, the encryption-context convention, the rotation model |
encryptor_ecto | The Ecto types, the schema conventions, the wrapped-key storage and its migration, the re-encryption migrator |
The split is deliberate and it is a boundary, not a layering convenience: no function in this package takes a repo, a query, a table, or a batch size, and this package defines no storage schema at all.
Engine notes
The design is written against aws_encryption_sdk v1.0.0 as published, with
module paths cited so every claim can be re-checked. Two upstream issues are
open and the design works around both until they move:
- #95 - the materials cache is unbounded and is not substitutable through the cache behaviour, so this package bounds it by recycling the cache process.
- #96 - a warm decryption cache bypasses reproduced-context validation, so this package performs the value comparison itself, above the engine. That is what makes anti-substitution a property of this package rather than one it happens to inherit.
Installation
def deps do
[
{:encryptor, "~> 0.1"}
]
endNot yet published to Hex.
License
Apache-2.0 - see LICENSE.