This file defines the bytes stored by Encrypted version 1. Treat the format, context packing, and key derivation as one compatibility contract.

Envelope

Version 1 uses this layout:

<<version::8, key_id::8, iv::binary-size(12),
  tag::binary-size(16), ciphertext::binary>>
OffsetSizeEncodingMeaning
01 byteunsigned integerversion; must equal 1
11 byteunsigned integermaster-key ID from 0 through 255
212 bytesraw bytesAES-GCM IV
1416 bytesraw bytesAES-GCM authentication tag
30remaining bytesraw bytesAES-256-GCM ciphertext

The minimum envelope is 30 bytes and represents an encrypted empty binary. The ciphertext segment has the same byte length as the plaintext.

The version byte stays first in every format. A reader must reject an unknown version. It must not interpret the remaining bytes using the version 1 layout.

Context packing

The format packs a list of binary items without delimiters:

item_count      := unsigned little-endian 32-bit integer
item            := byte_length || bytes
byte_length     := unsigned little-endian 64-bit integer
packed(parts)   := item_count || item_1 || ... || item_n

Version 1 context uses:

packed([
  "ecto_encrypted/aad/v1",
  table,
  storage_field
])

Lengths count bytes, not Unicode code points or graphemes. Table and field values are converted through String.Chars and must not be empty.

The complete AES-GCM associated data is:

<<version, key_id>> || packed_context

The cleartext header lets a reader choose a format and key. Including those two bytes in the associated data prevents an attacker from changing either choice without invalidating the authentication tag.

The context does not include a primary key. A valid ciphertext can therefore move between rows in the same table and column. Including row identity would make inserts, primary-key changes, bulk loading, and rotation depend on data that Ecto.ParameterizedType does not receive.

Per-field key derivation

The master key is 32 raw bytes. Configuration may express it as 64 hexadecimal digits; decoding occurs before derivation.

Version 1 derives a 32-byte field key with RFC 5869 HKDF-SHA-384:

salt = SHA-384("ecto_encrypted/hkdf-salt/v1")

info = packed([
  "ecto_encrypted/field-key/v1",
  table,
  storage_field
])

PRK = HMAC-SHA-384(salt, master_key)
T1  = HMAC-SHA-384(PRK, info || 0x01)
field_key = first_32_bytes(T1)

The encryption domains differ from the domains used by ecto_blind_search. Applications must still use separate master keys so that the two systems can rotate independently.

Encryption

Version 1 uses only AES-256-GCM:

ciphertext, tag = AES-256-GCM-ENCRYPT(
  field_key,
  12_random_bytes,
  plaintext,
  complete_associated_data,
  tag_length = 16
)

OTP's :crypto.crypto_one_time_aead/7 performs encryption and decryption. The package does not implement AES, GCM, SHA-384, or HMAC.

Version 1 uses the random-IV construction defined by NIST SP 800-38D, Section 8.2.2. Section 8.3 limits authenticated-encryption invocations to 2^32 for one derived field key across all application instances. The format contains no invocation counter. An operator must rotate to different master-key material before one table-and-storage-column context reaches the limit. A new key ID backed by unchanged master-key bytes derives the same field key and does not reset the count.

Pinned vector

Inputs:

master_key =
  0000000000000000000000000000000000000000000000000000000000000000

table         = "users"
storage_field = "email"
key_id        = 7
iv            = 101112131415161718191a1b
plaintext     = 766563746f7220706c61696e7465787400

Derived values:

field_key =
  71edfd3a21c5850a0a0f6d95afc6432901bad2c04520e56c4ba2b99c709aa035

packed_context =
  0300000015000000000000006563746f5f656e637279707465642f6161642f7631
  050000000000000075736572730500000000000000656d61696c

tag =
  1141bae68176d4be3989ff3a87af42c2

ciphertext =
  71ae086004c63111b82f7fa1968a47159c

Complete envelope:

0107101112131415161718191a1b1141bae68176d4be3989ff3a87af42c2
71ae086004c63111b82f7fa1968a47159c

The test suite checks this complete envelope and flips every byte in turn. Each modified envelope must fail before returning plaintext.

Version changes

Create a new envelope version before changing any of these values:

  • cipher or mode;
  • version or key-ID width;
  • IV or tag size;
  • envelope field order;
  • associated-data bytes or packing;
  • context fields;
  • key derivation, salt, domain, hash, or output length; or
  • compression policy.

Do not make a reader infer a format from length or successful decryption.