barrel_crypto_file (barrel_crypto v1.0.0)

View Source

Sector encryption for mmap'd flat files (BM25 disk, DiskANN).

These files are read at arbitrary byte offsets through iommap:pread or file:pread, so encryption must be offset-addressable (decrypt-after-read), not a stream. Two modes, chosen by how a file is rewritten:

  • STATIC: one 8-byte file nonce, keystream positioned by byte offset (<<Nonce:8/binary, Block:64>> counter blocks). Ciphertext length equals plaintext length and any slice decrypts independently. ONLY for files never rewritten in place: immutable or append-only files, with the nonce rotated on every full rewrite (compaction). Rewriting an offset under the same nonce reuses CTR keystream and breaks confidentiality.
  • EMBEDDED: fixed 4096-byte physical sectors, each carrying a fresh 12-byte nonce and a 4084-byte CTR payload. Safe for in-place rewrites (every write draws a new nonce). Logical bytes are the concatenation of payloads; readers translate logical offsets to sectors.

Neither mode authenticates sector contents (CTR is malleable), which matches the guarantee of RocksDB's EncryptedEnv: the threat model is data at rest. Wrong-key detection comes from the key-check token in the cleartext "BC" superblock a caller stores in its metadata file.

Summary

Functions

En/decrypt Bin at ByteOffset within the file's CTR stream. The call is symmetric. The caller owns nonce discipline: never rewrite a byte range in place under the same nonce.

Wrap a physical pread-style fun into one that reads decrypted bytes, so file readers change only where they build their read fun.

Open consecutive sectors back to the logical bytes (including the last sector's zero padding; callers track their own lengths).

Open one 4096-byte sector back to its 4084-byte payload (the caller's format carries its own lengths inside the payload).

Seal a whole logical binary into consecutive sectors.

Seal one payload into a full 4096-byte sector: fresh 12-byte nonce + CTR payload, zero-padded to the payload size. Payloads over 4084 bytes do not fit and raise {payload_too_large, Size}.

Decode a superblock. plaintext means the bytes do not start with the magic (the file predates encryption or is not encrypted); anything magic-prefixed that fails to decode is corrupt.

Encode the cleartext crypto superblock a caller stores in its metadata file: "BC" magic, version, and a small map that must carry key_check (a barrel_crypto:key_check_new/1 token) and may carry per-file nonces or anything else the caller needs.

Types

readfun/0

-type readfun() :: fun((non_neg_integer(), non_neg_integer()) -> {ok, binary()} | eof | {error, term()}).

Functions

crypt_static(Key, FileNonce, ByteOffset, Bin)

-spec crypt_static(binary(), binary(), non_neg_integer(), binary()) -> binary().

En/decrypt Bin at ByteOffset within the file's CTR stream. The call is symmetric. The caller owns nonce discipline: never rewrite a byte range in place under the same nonce.

decrypting_readfun(_, Key, PhysRead)

-spec decrypting_readfun({static, binary()} | embedded, binary(), readfun()) -> readfun().

Wrap a physical pread-style fun into one that reads decrypted bytes, so file readers change only where they build their read fun.

{static, Nonce}: physical and logical offsets coincide; each slice is decrypted at its own offset.

embedded: logical offsets address the payload stream; the wrapper reads the covering sectors, decrypts each, and slices. A short physical read means a truncated sector and fails closed.

open_logical(Key, Sectors)

-spec open_logical(binary(), binary()) -> binary().

Open consecutive sectors back to the logical bytes (including the last sector's zero padding; callers track their own lengths).

open_sector(Key, _)

-spec open_sector(binary(), binary()) -> binary().

Open one 4096-byte sector back to its 4084-byte payload (the caller's format carries its own lengths inside the payload).

payload_size()

-spec payload_size() -> pos_integer().

seal_logical(Key, Bin)

-spec seal_logical(binary(), binary()) -> binary().

Seal a whole logical binary into consecutive sectors.

seal_sector(Key, Payload)

-spec seal_sector(binary(), binary()) -> binary().

Seal one payload into a full 4096-byte sector: fresh 12-byte nonce + CTR payload, zero-padded to the payload size. Payloads over 4084 bytes do not fit and raise {payload_too_large, Size}.

sector_size()

-spec sector_size() -> pos_integer().

superblock_decode(_)

-spec superblock_decode(binary()) -> {ok, map()} | plaintext | {error, corrupt_superblock}.

Decode a superblock. plaintext means the bytes do not start with the magic (the file predates encryption or is not encrypted); anything magic-prefixed that fails to decode is corrupt.

superblock_encode(Map)

-spec superblock_encode(#{key_check := binary(), _ => _}) -> binary().

Encode the cleartext crypto superblock a caller stores in its metadata file: "BC" magic, version, and a small map that must carry key_check (a barrel_crypto:key_check_new/1 token) and may carry per-file nonces or anything else the caller needs.