Encryptor.Provider.Function (Encryptor v0.2.0)

Copy Markdown View Source

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 a FunctionClauseError from 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

opts()

@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.

state()

@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

decryption_keys(map, selector)

@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.

encryption_key(map, selector)

@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.

init(opts)

@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.