Encryptor.Vault.Partition (Encryptor v0.8.0)

Copy Markdown View Source

Derives the fixed-width cache partition id a vault hands the caching CMM.

One cache process serves every partition within a vault (ADR-0001 decision 3), so the thing that keeps one scope's data key out of another scope's cache lookup is the partition id, not a second process. Decision 7 fixes the derivation:

partition_id = binary_part(sha256(vault_namespace, 0, encoded_selector), 0, 16)

and this module is the only place it is computed. The result is what the vault hands the engine's caching CMM as :partition_id.

Both sides also carry the key, and the key's bytes

The engine's encryption cache id hashes the partition id, the suite and the context, and never the key that wrapped the entry's data key. With the formula above alone, a write after a new key version is minted finds the warm entry from before the mint and wraps its data key under the old version. So the partition a vault hands the caching CMM on the write side (an encrypt, and a rekey's write half) also carries the resolved key's identity - the namespace and name of an Encryptor.Key.Aes, the key id of an Encryptor.Key.Kms - in a length-prefixed pre-image, and a new version is a new partition, cold at once (ADR-0001 Amendment B).

A name is not enough on its own. A whole-scope shred deletes a store's rows, and a provision for the same scope at the same version mints the same name over new bytes. With names alone, a decryption entry warmed before the shred then serves the old message again, and the next write reuses an encryption entry whose data key is wrapped under the destroyed bytes. So an AES key's identity also carries a fingerprint of its material, and the read side (a decrypt, and a rekey's read half) is partitioned by the identity of every candidate the provider answered, in order (ADR-0001 Amendment C). Two different byte strings under one name never share a cache entry, on either side.

The fingerprint is a domain-separated SHA-256 of the material, computed in the pre-image and never kept: what leaves this module is the 16-byte truncation of a hash over it, which is not key material and is never put in telemetry, a message or a store. A Encryptor.Key.Kms key carries no material here; its key id names bytes that only the key manager holds.

Why the width is fixed, and why it is 16

The engine's cache-id computation (compute_encryption_cache_id/3 in the aws_encryption_sdk caching CMM) concatenates the partition id into the cache id pre-image with no length prefix. A variable-width partition id therefore makes the pre-image ambiguous, and two different partitions could in principle hash to one cache id - which is two scopes sharing a data key. Sixteen bytes is the width of the UUID the engine generates when no partition id is given, so matching it removes the ambiguity by construction rather than by argument.

Nothing here may be relaxed into "any binary": the width is load-bearing.

What a partition id is not

It is a cache-key input only. It is not key material, it is not secret, and it never reaches a message. Deriving it by hash rather than using the raw selector keeps scope identifiers out of a structure this package does not control the lifetime of, and buys the uniform width for free.

Records: ADR-0001 decisions 3 and 7 and Amendments B and C; the selector type is ADR-0004 decision 3.

Summary

Functions

The width every partition id has, in bytes.

The 16-byte partition id for a vault and a key selector.

Functions

bytes()

@spec bytes() :: pos_integer()

The width every partition id has, in bytes.

iex> Encryptor.Vault.Partition.bytes()
16

id(vault, selector)

@spec id(module(), Encryptor.Error.selector()) :: binary()

The 16-byte partition id for a vault and a key selector.

This is ADR-0001 decision 7's selector-only formula. The vault no longer hands it to the caching CMM: both sides partition by the keys as well (Amendments B and C), through derivations that are internal to the vault.

Pure, total over the selector types ADR-0004 decision 3 admits, and allocating nothing that outlives the call.

iex> id = Encryptor.Vault.Partition.id(MyApp.Vault, "scope-42")
iex> byte_size(id)
16

iex> Encryptor.Vault.Partition.id(MyApp.Vault, "scope-42") ==
...>   Encryptor.Vault.Partition.id(MyApp.Vault, "scope-43")
false

iex> Encryptor.Vault.Partition.id(MyApp.Vault, :default) ==
...>   Encryptor.Vault.Partition.id(MyApp.OtherVault, :default)
false