A provider built from a host-supplied pair of closures.
The escape hatch. It exists so the per-tenant case is reachable on day one, before a storage-backed adapter exists, and so a host that already has a key store can use this package without writing a behaviour implementation.
provider: {Encryptor.Provider.Function,
encryption_key: fn merchant_id -> MyApp.Keys.current(merchant_id) end,
decryption_keys: fn merchant_id -> MyApp.Keys.live(merchant_id) end}Both closures take one argument, the selector, and return
{:ok, descriptor} / {:ok, [descriptor, ...]} or {:error, reason} with
a reason from the provider vocabulary. Anything a closure needs beyond the
selector it captures.
It inherits every obligation and enforces none of them
A closure pair is unreviewed code on the key path. Nothing here stops one
from blocking for thirty seconds, reusing a name for different bytes, or
caching unboundedly - the obligations in Encryptor.Provider are the
contract, and this provider is the shape that makes breaking them easiest.
Shipping it anyway is the deliberate trade: refusing a function-shaped
provider would push hosts into forking the package, which is worse.
What it does check, on the way back
The two things a wrong answer would otherwise turn into a confusing failure somewhere else:
- The shape of the answer. A closure that returns a bare descriptor, a
map,
nil, or an empty candidate list gets{:invalid_key_descriptor, detail}- a bug in the provider, named as one, rather than aFunctionClauseErrorfrom inside the vault. - That descriptors are members of the closed set. An engine keyring, or a host struct that looks like a descriptor, is refused here.
Field-level validation - the reserved namespace prefix, printability, the material length against the declared size - stays the vault's, immediately before it builds a keyring. This is a membership check, not a second copy of that one.
A closure's {:error, reason} passes through when the reason is one of the
five in the provider vocabulary. Anything else becomes
{:invalid_key_descriptor, {:unrecognized_reason, tag}}, carrying the
leading tag and nothing else: the vocabulary is closed so that a case over
it is exhaustive, and a term that escaped into it would arrive at a failure
renderer that has no clause for it. The tag alone is carried because a
reason term from a host closure can hold anything, including key material.
The detail term never carries the answer
Every {:invalid_key_descriptor, detail} here names the constraint that was
violated and, at most, the struct module involved. The value that violated
it is not carried: it came from a closure that resolves key material, and
the error it lands in is one a host may well log.
Records: ADR-0002 decisions 1, 3, 5 and 6.
Summary
Types
The two closures, both required.
The frozen state: the two closures, checked for arity at start.
Functions
Calls the host's decryption closure and validates its answer: a non-empty list, every member a descriptor.
Calls the host's encryption closure and validates its answer.
Holds the two closures, having checked that both are present and take one argument. A missing or wrong-arity closure fails the vault's start rather than the host's first encrypt.
Types
@type opts() :: [ encryption_key: (Encryptor.Provider.selector() -> {:ok, Encryptor.Provider.descriptor()} | {:error, term()}), decryption_keys: (Encryptor.Provider.selector() -> {:ok, [Encryptor.Provider.descriptor(), ...]} | {:error, term()}) ]
The two closures, both required.
@type state() :: %{ encryption_key: (Encryptor.Provider.selector() -> term()), decryption_keys: (Encryptor.Provider.selector() -> term()) }
The frozen state: the two closures, checked for arity at start.
Functions
@spec decryption_keys(state(), Encryptor.Provider.selector()) :: {:ok, [Encryptor.Provider.descriptor(), ...]} | {:error, Encryptor.Provider.reason()}
Calls the host's decryption closure and validates its answer: a non-empty list, every member a descriptor.
@spec encryption_key(state(), Encryptor.Provider.selector()) :: {:ok, Encryptor.Provider.descriptor()} | {:error, Encryptor.Provider.reason()}
Calls the host's encryption closure and validates its answer.
@spec init(opts()) :: {:ok, state()} | {:error, Encryptor.Error.reason()}
Holds the two closures, having checked that both are present and take one argument. A missing or wrong-arity closure fails the vault's start rather than the host's first encrypt.