| Internet-Draft | SAFE | July 2026 |
| Sullivan | Expires 17 January 2027 | [Page] |
SAFE defines an encryption envelope that encrypts a payload once for multiple recipients. Decryption can require multiple credentials in sequence (public keys, passphrases, or other registered methods), so that no single compromise reveals content. The format targets large, writable files: it supports streaming decryption, random-access reads at segment granularity, and selective re-encryption of modified segments without re-keying. Per-segment Authenticated Encryption with Associated Data (AEAD) provides confidentiality and integrity and detects reordering, truncation, and extension. SAFE accommodates post-quantum key encapsulation without format changes, provides algorithm agility through IANA registries, supports recipient privacy, and defines application profiles for common deployment scenarios.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 17 January 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
SAFE is an encryption format for files and objects. A SAFE-encoded file contains an encrypted payload and one or more LOCK blocks. Each LOCK defines one independently satisfiable access path to the Content-Encryption Key (CEK). Multiple LOCKs let different recipients, roles, groups, or recovery mechanisms decrypt the same payload without duplicating ciphertext. Within a LOCK, all listed Steps are required, so one LOCK can demand several credentials together (a passphrase AND a private key, for example).¶
The payload is split into fixed-size segments, each encrypted with Authenticated Encryption with Associated Data (AEAD) [RFC5116] and a per-segment nonce, random or derived per the selected profile. Segments can be decrypted individually or streamed sequentially. An aligned binary layout (Section 6.4.2) places each ciphertext segment at a predictable offset, enabling O(1) random reads. Per-segment nonces (Section 5.8.4) allow individual segments to be re-encrypted without re-wrapping the CEK or touching other segments.¶
Existing formats address subsets of these capabilities; Section 2 surveys the differences. SAFE provides algorithm agility through IANA registries (Section 10) and accommodates post-quantum key encapsulation mechanisms without format changes. Recipient privacy modes (Section 8.7) allow Hybrid Public Key Encryption (HPKE) steps to omit key identifiers, preventing passive observers from linking files to recipients. Application profiles (Section 7.4) provide baseline algorithm guidance for common deployment scenarios.¶
For example, a deployment might require that documents be decryptable only with both a passphrase AND a recipient private key. The LOCK block for such a recipient would contain two Step lines:¶
Step: pass(kdf=argon2id, salt=...) Step: hpke(kem=p-256, id=..., kemct=...)¶
Both steps are evaluated with the known passphrase and private key to derive the key that wraps the CEK. Neither factor alone suffices. See Section 5.7.1.1 for the cryptographic details.¶
The payload layer and LOCK layer serve distinct roles. The LOCK layer manages key encapsulation and multi-factor access control, wrapping the CEK independently for each recipient. The payload layer provides per-segment authenticated encryption with support for random-access reads and selective re-encryption of modified segments.¶
Within the payload layer, a file-level commitment value precedes independently decryptable fixed-size segments. Confidentiality and integrity are enforced per segment. Segment AAD authenticates both the segment index and a final-segment indicator, so a Decryptor can detect segment substitution, reordering, truncation, and extension (Section 5.8.8). A commitment prefix derived from the CEK, the negotiated algorithm parameters, and a per-file salt provides uniform key commitment across all AEAD choices (Section 8.13).¶
The Profile CONFIG field (Section 6.1) selects one of the named SEAL instantiations (Section 5.8.1). The mutable instantiations permit in-place rewrite of individual segments; the immutable ones follow a write-once discipline, encrypting each segment exactly once.¶
The payload layer of SAFE is a profile of SEAL, the Segmented Encrypted Authentication Layer construction, specified in [I-D.sullivan-cfrg-raae]; see Section 5.8.1. [FLOE] formalizes the security model as ra-ROR (random-access real-or-random) and ra-CMT (random-access context commitment). It proves that any such construction achieves both properties independently of the underlying AEAD's own commitment behavior.¶
SAFE's registered AEADs fall into two usage classes: Misuse-Resistant Authenticated Encryption (MRAE, [RFC9771]) and non-MRAE suites. Non-MRAE suites (AES-256-GCM, ChaCha20-Poly1305, AEGIS-256, AEGIS-256X2) depend on per-segment nonce uniqueness and the total number of segment encryptions under a given payload key. They store a random per-segment nonce under a Profile whose nonce mode is random, and use derived nonces under a write-once Profile, where each derived nonce is used exactly once (Section 5.8.1, Section 5.8.4). The MRAE suite (AES-256-GCM-SIV, where SIV denotes Synthetic Initialization Vector) tolerates nonce reuse, degrading to deterministic encryption rather than permitting plaintext recovery. Because segment rewrites consume additional encryptions under the same payload key, the relevant usage budget is not just the current file size but the total lifetime segment encryptions, including all rewrites. See Section 8.14 for per-AEAD analysis.¶
This section summarizes the encryption and decryption procedures. Normative details appear in the referenced sections.¶
Readers can follow a profile-keyed path through the rest of this document. The Profile field, the named instantiations it selects, and the guarantees each delivers are given in Section 5.8.1.¶
Implementers of the default mutable object (the SEAL-editable Profile, and likewise SEAL-memory and SEAL-compact) read:¶
Section 6.1 and Section 5.1, for parameter selection and defaults;¶
Section 5.8.2, for the payload key schedule;¶
Section 5.8.7, for the snapshot authenticator;¶
Section 5.8.6, for rewrite, extend, and truncate.¶
Implementers of immutable write-once objects (the SEAL-simple, SEAL-attachment, and SEAL-attachment-small Profiles) read Section 6.1, Section 5.1, and Section 5.8.2. They MAY skip Section 5.8.6, which does not apply. Under SEAL-simple they MAY also skip Section 5.8.7; SEAL-attachment and SEAL-attachment-small carry a snapshot and need it.¶
A SAFE object is the abstract value that the format encodes. It has three parts: a Suite, a set of LOCKs, and a Payload. The Suite names the cryptographic parameters that govern the object (Profile, AEAD, and KDF). Each LOCK carries an ordered list of steps and one Encrypted-CEK, and represents a single way to recover the CEK. The Payload is the segmented, authenticated ciphertext together with the per-file salt, commitment, and snapshot authenticator. Multiple LOCKs wrap the same CEK, so adding or removing a LOCK changes who can open the object without re-encrypting the Payload.¶
A step has three forms, distinguished by where it lives. The step_descriptor is the on-wire text that appears verbatim in a LOCK block (for example, hpke(kem=x25519, kemct=..., id=...)). The step_binding is the canonical Encode() form bound into the LOCK key derivation: display-only fields are stripped and the key identifier is reconstructed even when omitted on the wire, so a step is bound to the material it actually used. The step_secret is the 32-octet secret a credential yields for that step. The LOCK key derivation (Section 5.7.1) folds the ordered step secrets and step bindings of a LOCK into that LOCK's KEK, and the payload layer is a profile of SEAL (Section 5.8.1).¶
Serialization is separate from the object. A LOCK is written in a readable text encoding or an armored encoding (Section 6.2), and the Payload is written in an armored or binary encoding (Section 6.3); these encodings are lossless transcodings of one another, and the choice of encoding does not change the object's cryptographic value or any bound parameter.¶
Given a plaintext and a set of recipients (each defined by one or more credentials), an Encryptor produces a SAFE object:¶
Select a Profile, AEAD, and KDF (or use defaults).¶
Generate a random 32-octet CEK: SafeRandom(32, "SAFE-CEK")
(Section 5.4.2).¶
For each recipient, build a LOCK: generate step artifacts (salts, KEM ciphertexts), derive a KEK, and wrap the CEK.¶
Generate a 32-octet per-file salt:
SafeRandom(32, "SAFE-SALT").¶
Derive payload_key and snap_key from CEK and salt (Section 5.8.2).¶
Split plaintext into segments; encrypt each with a per-segment nonce.¶
Compute the snapshot authenticator over the per-segment inputs it defines (Section 5.8.7).¶
Write the file: optional CONFIG, LOCK blocks, and payload (the salt leads the payload's raAE header, Section 6.4).¶
Given a SAFE object and the appropriate credentials (private keys, passphrases, or other step inputs), a Decryptor recovers the plaintext:¶
Parse CONFIG, LOCK, and DATA blocks.¶
Try each LOCK until one succeeds: evaluate its steps with the recipient's credentials to derive a KEK, then unwrap the CEK.¶
Read the 32-octet salt, which leads the raAE header (Section 6.4).¶
Verify the commitment prefix.¶
Derive payload_key and snap_key from CEK and salt (Section 5.8.2).¶
Verify the snapshot as the resolved snap_id allows (Section 5.8.7): SnapVerify over the whole segment set at 0x0001, the authenticator's per-segment procedure at 0x0002 and 0x0003, and nothing at 0x0000.¶
Decrypt requested segments.¶
The CEK enables multi-recipient encryption (wrap once per recipient). The KEK binds each recipient's credentials to the CEK. The payload_key is derived from the CEK and the payload_info (the SEAL payload binding; Section 5.8.2), which carries the AEAD, segment size, KDF, snap_id, nonce mode, epoch length, and the per-file salt. This provides domain separation: distinct salts produce distinct payload keys even under the same CEK.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Header text is UTF-8. "Base64" in this document refers to the standard
Base64 encoding defined in Section 4 of [RFC4648]. The alphabet is
the 64-character set A-Z, a-z, 0-9, +, /, with = as the
padding character. The URL-safe alphabet defined in Section 5 of
[RFC4648] (which substitutes - for + and _ for /) MUST NOT be
used. Padding MUST be present; the unpadded form is not permitted.
Decryptors MUST reject any Base64 input containing characters outside
the standard alphabet, padding characters in invalid positions, or
control characters including CR and LF within a single Base64 field
value.¶
When Base64 values appear in SAFE blocks, Encryptors SHOULD wrap lines at 64 characters; Decryptors MUST accept any line length and MUST strip line breaks (LF and CRLF) before decoding. LF denotes the newline U+000A; Encryptors MUST use LF, and Decryptors MUST accept LF and MAY accept CRLF.¶
String constants used in Encode AAD labels are ASCII; key-layer labels begin with SAFE- (e.g., SAFE-STEP) and the payload AAD label is SEAL-DATA (Section 5.8.1). SafeDerive and SealDerive labels are ASCII (e.g., commit, kek), and the protocol prefix is added automatically: SAFE-v1 for the key layer, and SEAL-RW-v1 or SEAL-RO-v1 for the payload.¶
ABNF follows [RFC5234]. The following common ABNF rules are used throughout this document:¶
BASE64CHAR = ALPHA / DIGIT / "+" / "/" / "=" NZDIGIT = %x31-39 ; "1" through "9" UINT = "0" / (NZDIGIT *DIGIT)¶
BASE64CHAR is a loose character-class approximation; implementations MUST validate Base64 encoding per [RFC4648] Section 4, including correct padding placement. UINT is the canonical decimal ASCII encoding of a non-negative integer: the single character "0" for zero, or one or more decimal digits with no leading zero for positive values. No sign character, whitespace, or non-ASCII codepoint is permitted. Decryptors MUST reject any integer field whose value does not match UINT.¶
This document uses the following notation:¶
| Symbol | Meaning |
|---|---|
| || | Octet string concatenation |
XOR
|
Bitwise exclusive-or of equal-length octet strings |
len(x)
|
Length of x in octets |
x[i:j]
|
Slice of x from octet i (inclusive) to j (exclusive), zero-indexed |
uint8(n)
|
8-bit unsigned integer n (single octet) |
I2OSP(n, w)
|
w-octet big-endian encoding of non-negative integer n |
frame(x)
|
2-octet length-prefixed framing of x; total over all field lengths via a reserved 0xFFFF digest prefix (Section 4.1.1) |
Encode(x1, ..., xn)
|
frame(x1) || ... || frame(xn), the multi-value length-prefixed encoding |
...x
|
List expansion: if x is a list, each element becomes a separate argument; if x is a single octet string, it is passed as one argument |
uint64(n)
|
64-bit unsigned integer n in network byte order (big-endian) |
floor(x)
|
Largest integer less than or equal to x |
ceil(x)
|
Smallest integer greater than or equal to x |
All integers serialized in binary are unsigned and use network byte order (big-endian). Multi-byte integer fields are serialized most-significant byte first.¶
SAFE encodes its structured inputs with the length-prefixed framing of Section 4.3 (The KDF Combiner) of [I-D.sullivan-cfrg-raae]. frame emits a field of at most 0xFFFE octets literally behind a 2-octet length, the reserved length 0xFFFF introduces a fixed-size digest of an over-large field, and Encode concatenates the framed elements. SAFE uses this framing unchanged, so the key identifier (Section 5.6.3.3) works for a public key of any size: frame of the SPKI DER encoding takes the digest form automatically once it exceeds 0xFFFE octets.¶
A 0xFFFF entry is bind-only: it commits to the field but does not carry it. The SAFE KEM Identifiers registry (Section 10.2) and the SAFE Step Names registry (Section 10.4) MUST mark which fields a decoder must RECOVER from the encoding. Every recoverable field MUST be at most 0xFFFE octets so that it is always encoded literally; the set of recoverable fields is the closed set named by those registries, not an open-ended example. The recoverable fields defined today are the KEM ciphertext carried in a step_descriptor (Section 5.6.3) and the step parameters parsed from the descriptor. All KEM ciphertexts for registered KEMs (Section 5.6.3.2) satisfy this bound. A decoder that reads a 0xFFFF length prefix where a recoverable field is expected MUST treat it as a parse error and reject the input; it MUST NOT accept the entry as a bind-only commitment in that position.¶
SAFE header lines (fence markers, field names, field values) MUST contain only ASCII printable characters (0x20-0x7E) plus LF (0x0A). Derive info strings and AEAD AAD prefixes use ASCII. Decryptors MUST reject malformed UTF-8 in text fields.¶
Base64 encoding follows the rules in Section 4. Base64 values in headers MAY wrap across lines; continuation lines MUST begin with at least one space or tab. Decryptors MUST strip leading whitespace from continuation lines before concatenating and decoding.¶
Encryptors MUST use LF (0x0A) line terminators. Decryptors MUST accept LF and MAY accept CRLF. Decryptors MUST strip trailing whitespace from header lines.¶
All field names, identifiers, and fence markers are case-sensitive.¶
A randomly generated 32-octet key used to derive the payload encryption key. The CEK is wrapped independently for each recipient.¶
A 32-octet key derived from a LOCK's step sequence. Used to wrap or unwrap the CEK.¶
The party that creates a SAFE-encoded file.¶
The party that recovers plaintext from a SAFE-encoded file using appropriate credentials.¶
The following defaults apply whenever a CONFIG block is absent or when a field is omitted from the CONFIG block:¶
| Field | Default Value |
|---|---|
| Profile | SEAL-editable |
| AEAD | aes-256-gcm |
| KDF | hkdf-sha-256 |
| Lock-Encoding | armored |
| Data-Encoding | binary |
Implementations MUST use these values for any omitted fields. CONFIG need only include fields that differ from the defaults; see Section 6.1.¶
This section provides a quick reference of all cryptographic algorithms and identifiers used in SAFE. Detailed specifications appear in later sections.¶
| Algorithm | Identifier | Nk | Nn | MRAE |
|---|---|---|---|---|
| AES-256-GCM | aes-256-gcm | 32 | 12 | No |
| ChaCha20-Poly1305 | chacha20-poly1305 | 32 | 12 | No |
| AES-256-GCM-SIV | aes-256-gcm-siv | 32 | 12 | Yes |
| AEGIS-256 | aegis-256 | 32 | 32 | No |
| AEGIS-256X2 | aegis-256x2 | 32 | 32 | No |
Nk/Nn are key/nonce sizes in octets. "MRAE" indicates misuse-resistant authenticated encryption ([RFC9771]; see Section 8.10). Epoch length is not a CONFIG choice: the selected SEAL named instantiation resolves it (Section 5.8.1, Section 5.8.3). AEADs without MRAE permit plaintext recovery under nonce reuse; Encryptors SHOULD select an MRAE AEAD when nonce reuse cannot be ruled out. All AEADs provide [RFC5116] semantics with 16-octet tags. In every SAFE DATA payload a 32-octet salt leads the raAE header (Section 6.4), followed by an Nh-octet commitment prefix derived per Section 5.8.2 that binds the ciphertext to the CEK, the negotiated algorithm parameters, and the per-file salt (see Section 8.13).¶
| KEM | Identifier | HPKE KEM ID | Encap Size | Auth |
|---|---|---|---|---|
| X25519 | x25519 | 0x0020 | 32 octets | Yes |
| P-256 | p-256 | 0x0010 | 65 octets | Yes |
| ML-KEM-768 | ml-kem-768 | 0x0041 | 1088 octets | No |
HPKE KEM IDs are defined in Section 7.1 of [RFC9180] and the IANA HPKE KEM Identifiers registry. ML-KEM-768 enables hybrid post-quantum constructions via multi-step step sequences (see Section 5.6.3.2).¶
All KEMs use HPKE [RFC9180] in export-only mode (AEAD ID 0xFFFF). The
encryptor calls SetupBaseS (or SetupAuthS when sid or shint is
present) to produce a KEM ciphertext and an HPKE context, then calls
Export on the context to derive the step secret (Section 5.6.3.4).
When a sender parameter is present, HPKE Auth mode is used (see
Section 5.6.3.1). The KEM identifier appears in hpke(...) step
descriptors (Section 5.6.3). SAFE maintains a registry mapping string
identifiers to HPKE KEM IDs (Section 10.2).¶
| Step Type | Token Format | Parameters | Secret |
|---|---|---|---|
| Passphrase | pass(kdf=..., salt=...) | kdf, salt | 32 octets |
| HPKE | hpke(kem=X, kemct=..., ...) | kem, kemct, id/hint, sid/shint | 32 octets |
Step types are composed via multiple Step lines within a LOCK block, with AND semantics: all steps are required to derive the KEK. Each step conforms to the interface defined in Section 5.6 and produces a 32-octet secret that contributes to KEK derivation (Section 5.7.1). Additional step types MAY be registered per Section 10.4.¶
AEADs used with SAFE MUST provide [RFC5116] semantics with a 16-octet
authentication tag. KEMs MUST use HPKE export-only mode (AEAD ID
0xFFFF) as specified in Section 5.6.3. When sid or shint is present,
HPKE Auth mode is used (Section 5.6.3.1).¶
SAFE piggybacks on the HPKE KDF interface. HPKE [RFC9180] and [I-D.ietf-hpke-pq] define two KDF classes:¶
Two-stage KDFs (e.g., HKDF-SHA256, KDF ID 0x0001):¶
KDF.Nh: output size of Extract (32 for HKDF-SHA256)¶
KDF.Extract(salt, ikm) -> prk (Nh octets)¶
KDF.Expand(prk, info, L) -> okm (L octets)¶
Single-stage KDFs (e.g., TurboSHAKE256, [I-D.ietf-hpke-pq]):¶
KDF.Derive(ikm, L) -> okm (L octets).
For TurboSHAKE256: TurboSHAKE256(M=ikm, D=0x1F, L).¶
The KDF config parameter selects the key derivation function and its class. Any KDF registered for SAFE MUST be registered in the HPKE KDF Identifiers registry [RFC9180] and MUST implement either the two-stage (Extract/Expand/Nh) or single-stage (Derive) interface.¶
SafeDerive is the universal combiner of Section 4.3 (The KDF Combiner) of [I-D.sullivan-cfrg-raae], instantiated at protocol_id "SAFE-v1". SafeDerive(label, ikm, info, L) binds the protocol version, a call-site label, and the ikm and info inputs into an L-octet output. ikm and info each accept a single octet string or an ordered list, and the combiner frames each as one element, so any number of inputs of any size is admitted. Callers that require suite binding include the suite parameters in info. The key identifier (Section 5.6.3.3) omits them, but it is not config-independent. SafeDerive invokes the configured KDF to produce the identifier, so the identifier is bound to that KDF.¶
The key identifier (Section 5.6.3.3) and the per-LOCK key (Section 5.7.1) use SafeDerive. SealDerive is the same combiner at the payload protocol_id, which is SEAL-RW-v1 for the mutable instantiations and SEAL-RO-v1 for the immutable ones, selected by the Profile (Section 5.8.1). The payload layer (Section 5.8.2) adopts the selected SEAL named instantiation of [I-D.sullivan-cfrg-raae] verbatim.¶
SafeDerive labels MUST be unique across all call sites within a single protocol version. The labels defined by this specification are reserved; future extensions and step type registrations MUST NOT reuse them. Similarly, SafeRandom labels (Section 5.4.2) MUST be unique and MUST NOT be reused by extensions.¶
SafeRandom is the random generation function used for all encryptor-generated random values in SAFE. It requires a cryptographically secure pseudorandom number generator (CSPRNG).¶
SafeRandom(n, label):
return CSPRNG(n)
¶
When no private key is available, SafeRandom returns raw CSPRNG output. The label parameter is used for domain separation when hedging is enabled (Section 5.4.2.2); it is ignored in the base construction but callers MUST still supply it. This invariant keeps call sites consistent across deployments: if a hedge key becomes available later, the binding is correct without requiring code changes at every SafeRandom call site.¶
When a long-term private key sk is available, the Encryptor SHOULD mix
it into SafeRandom to defend against a weak or attacker-influenced RNG.
The construction follows [RFC8937]: hedge_key is a deterministic
function of sk (replacing the signature in [RFC8937] with a KDF,
since SAFE does not require a signature scheme), and SafeRandom combines
it with CSPRNG output via SafeDerive.¶
hedge_key = SafeDerive("SAFE-HEDGE", sk, "", 32)
SafeRandom(n, label):
return SafeDerive(label,
[hedge_key, CSPRNG(n)], "", n)
¶
An adversary who can predict CSPRNG output but does not know sk cannot
predict the hedged values. Hedging does not prevent repeated output
from RNG state duplication (VM snapshot restore, process fork without
reseed); identical CSPRNG output produces identical hedged output
regardless of the private key.¶
Suitable private keys include any long-term key held by the Encryptor (an HPKE sender private key, an application-provided signing key, or similar). The key need not correspond to any LOCK step.¶
Encryptors MUST use SafeRandom for all random values generated during
SAFE encoding, each under its own reserved label: the CEK
(SafeRandom(32, "SAFE-CEK")), the per-file payload salt
(SafeRandom(32, "SAFE-SALT")), the per-step passphrase salt
(SafeRandom(16, "SAFE-PASS-SALT")), lock_nonce (SafeRandom(Nn,
"SAFE-LOCK-NONCE")), and per-segment nonces in random mode
(SafeRandom(Nn, "SAFE-NONCE")). HPKE internal randomness (Encap) is
not hedged by default. Implementations whose HPKE library accepts an
external randomness source SHOULD supply SafeRandom(Nrand, "SAFE-ENCAP")
instead of raw CSPRNG output, where Nrand is the randomness length
required by the KEM.¶
A functioning CSPRNG is REQUIRED when no private key is available. See Section 8.10 for the security analysis.¶
The encryption_parameters input is an ordered list of five elements derived from the effective parameters (defaults augmented by any config overrides). The aead_id, kdf_id, and snap_id are 2-octet big-endian numeric identifiers, segment_size_be is a 4-octet integer, and epoch_length_u8 is a single octet. It never appears on the wire; Encryptors and Decryptors compute it from the effective parameters. The readable CONFIG tokens are unchanged and appear only in CONFIG.¶
encryption_parameters = [aead_id, segment_size_be, kdf_id, snap_id,
epoch_length_u8]
¶
I2OSP(code point, 2): the 2-octet big-endian AEAD ID of the effective AEAD parameter, taken from the AEAD ID column of Section 10.1.¶
I2OSP(Segment-Size, 4): the effective Segment-Size as a 4-octet big-endian integer.¶
I2OSP(code point, 2): the 2-octet big-endian HPKE KDF ID of the effective KDF parameter, taken from the HPKE KDF ID column of Section 10.3. The KDF ID identifies the construction (HKDF [RFC5869] with the selected hash, or a single-stage KDF).¶
I2OSP(code point, 2): the 2-octet big-endian snapshot-authenticator identifier fixed by the Profile (Section 5.8.1). 0x0000 is no snapshot; the other values name an authenticator registered in Section 4.7 of [I-D.sullivan-cfrg-raae].¶
I2OSP(r, 1) where r is the epoch_length the selected SEAL named instantiation resolves to (Section 4.12 of [I-D.sullivan-cfrg-raae]), including that document's treatment of the 256-bit-nonce suites; see Section 5.8.3.¶
The list always has exactly five elements. The aead_id and kdf_id MUST equal the registered code points (Section 10.1 and the HPKE KDF registry Section 10.3). The snap_id MUST be the one the Profile fixes (Section 5.8.1). Because the Profile, AEAD, and KDF fix these elements, changing any of them requires re-encryption of the payload and re-wrapping of the CEK in new LOCKs.¶
SafeDerive splices this list via ...encryption_parameters; each
element is individually framed (Section 4.1.1).¶
All algorithm and step identifiers in CONFIG and LOCKs are case-sensitive ASCII strings. Implementations MUST compare these identifiers byte-exact. Case-insensitive comparison, Unicode normalization, and whitespace trimming MUST NOT be applied to identifier values before comparison. The numeric identifiers in encryption_parameters (aead_id, kdf_id, snap_id) compare as the integers they encode.¶
SafeDerive binds encryption_parameters in the LOCK's single KEK derivation (Section 5.7.1). The payload schedule binds a distinct input, payload_info (Section 5.8.2): the SEAL list that additionally carries nonce_mode and the per-file salt. The two lists diverge at nonce_mode (Section 5.8.2). See the worked example in Appendix G.¶
Step terminology at a glance:¶
A step is a registered cryptographic operation that produces a 32-octet step secret from user-supplied credentials or cryptographic material. Each step type MUST define:¶
A unique ASCII identifier used in step descriptors (e.g., "pass", "hpke").¶
An ABNF grammar for step-specific parameters appearing in the token (e.g., kem=x25519, id=...).¶
A deterministic algorithm that produces exactly 32 octets. The algorithm MUST be reproducible given the same inputs. The step definition MUST specify all required inputs for both encryption and decryption.¶
The step secret and step_binding feed into the LOCK's single KEK derivation (Section 5.7.1). The ordered step secrets form the ikm argument to SafeDerive, and the encryption parameters followed by the ordered step bindings form the info argument. SafeDerive maps these logical inputs to the selected SAFE KDF class. Each step has two encodings: the step_descriptor is the on-wire form that appears verbatim in the LOCK block; the step_binding is the canonical binary encoding of its cryptographically relevant fields via Encode() (Section 4.1) and serves as the binding form in the KEK schedule. Display-only fields (label, hint, shint) are excluded from the step_binding. The step_bindings for the built-in step types are:¶
| Step | On-wire descriptor | step_binding |
|---|---|---|
| pass | pass(kdf, salt) | Encode("pass", kdf, salt) |
| hpke | hpke(kem, kemct, [id/hint]) | Encode("hpke", kem, kemct, id) |
| hpke (auth) | hpke(kem, kemct, [id/hint], [sid/shint]) | Encode("hpke", kem, kemct, id, "auth", sid) |
String values (kdf, kem) are UTF-8; binary values (salt, kemct, id, sid) are raw decoded octets, not Base64.¶
Fields computed for binding (hpke id, hpke sid, webauthn-prf rpid)
may be omitted on-wire for privacy but are deterministically
reconstructed during decryption and always appear in the
step_binding. For hpke, id is always present in the step_binding
even when omitted from the on-wire descriptor; it is computed during
trial decryption. Similarly, sid is optional on-wire but always
present in auth-mode binding; when omitted, it is computed from the
candidate sender public key.¶
New step types are registered via the IANA SAFE Step Names registry (Section 10.4) with Specification Required policy. The registration MUST include: step name, parameters grammar, inputs, derivation algorithm, Encode binding form, and any step-specific parameter definitions. See Appendix I for an example.¶
A human-readable display name intended to help users identify which
passphrase, credential, or key to use during decryption (e.g., "Work
laptop", "Recovery key"). The label is always excluded from the
step_binding and has no cryptographic effect. Encryptors MAY
include a label in any step descriptor; Decryptors MUST ignore it
for binding purposes. The label value MUST match the grammar
1*(ALPHA / DIGIT / "-").¶
Steps are registered in the IANA SAFE Step Names registry (Section 10.4). The following subsections define the initial registered steps.¶
The passphrase step derives a 32-octet step secret from a user
passphrase using a password-based KDF. The kdf parameter selects the
algorithm:¶
| KDF | Algorithm | Parameters |
|---|---|---|
| argon2id | Argon2id [RFC9106] | m=65536 KiB, t=2, p=1 |
| pbkdf2 | PBKDF2-HMAC-SHA-256 [RFC8018] | iter=600000 |
The step descriptor format is:¶
pass(kdf=<kdf>,salt=<Base64>) pass(kdf=<kdf>,salt=<Base64>,label=<text>)¶
The kdf and salt parameters are REQUIRED. The label parameter is
OPTIONAL and is for display only; it is not included in the step_binding
(Section 5.6). Encryptors MUST generate a fresh 16-octet salt using
SafeRandom(16, "SAFE-PASS-SALT") for each pass(...) step in a LOCK.
Decryptors MUST reject pass(...) steps whose salt value does not
decode to exactly 16 octets.¶
Grammar:¶
pass-step = "pass(" pass-params ")"
pass-params = "kdf=" kdf-name "," "salt=" salt
[ "," "label=" label-value ]
kdf-name = "argon2id" / "pbkdf2"
salt = 1*BASE64CHAR
label-value = 1*( ALPHA / DIGIT / "-" )
¶
Encode form:¶
Encode("pass", kdf, salt)
¶
step_binding: Encode("pass", kdf, salt).¶
The step secret is computed as follows:¶
Argon2id(passphrase, salt, m=65536, t=2, p=1, T=32) per Section 3.1 of [RFC9106].¶
PBKDF2(PRF=HMAC-SHA-256, Password=passphrase, Salt=salt, c=600000, dkLen=32).¶
In both cases, salt is the decoded value of the salt parameter.¶
Implementations SHOULD prefer argon2id for its memory-hardness properties. Implementations MAY support pbkdf2 for environments where Argon2id is not permitted by policy.¶
The HPKE step descriptor has three forms:¶
hpke(kem=x25519, kemct=<Base64>, id=<Base64>) ; Identified mode hpke(kem=x25519, kemct=<Base64>, hint=<digits>) ; Hinted mode hpke(kem=x25519, kemct=<Base64>) ; Anonymous mode¶
The parameters are:¶
The KEM algorithm. Supported values: x25519, p-256, ml-kem-768.¶
The Base64-encoded HPKE KEM encapsulated key material (the KEM ciphertext). This value MUST decode to the encapsulated key length for the selected KEM (see Section 5.2.2). Decryptors MUST reject hpke steps whose decoded kemct length does not match the KEM's encapsulated key length.¶
The key identifier computed as SafeDerive("SAFE-SPKI-v1", spki_der, "", 32) using the configured KDF (default: hkdf-sha-256). When present, Decryptors match this value against their local keys. When omitted, Decryptors perform trial decryption. See Section 5.6.3.3 and Section 8.7.¶
A 4-digit decimal value (0000-9999) assigned by the recipient out-of-band; not solely dependent on the public key. When present, Decryptors filter candidate keys to those associated with this hint in their local key storage. Mutually exclusive with id.¶
Encryptors MUST include exactly one of: id, hint, or neither (but
not both id and hint).¶
In Base mode, any party who knows a recipient's public key can create a valid SAFE object for that recipient. Auth mode [RFC9180] uses SetupAuthS/SetupAuthR, which bind the HPKE context to the sender's private key so that the Decryptor can verify who produced the object. This is useful for offline encrypted file exchange where the recipient needs assurance of origin (for example, encrypted firmware images, signed-then-encrypted document workflows, or air-gapped key escrow) without requiring a separate signature layer.¶
The presence of sid or shint selects HPKE Auth mode (mode_auth)
instead of Base mode. Auth mode MUST only be used with Diffie-Hellman
KEM (DHKEM) based KEMs (x25519, p-256). Encryptors MUST NOT include
sid or shint with ml-kem-768 or other non-DHKEM KEMs, because these
KEMs do not define AuthEncap/AuthDecap.¶
The sender's key identifier, computed as SafeDerive("SAFE-SPKI-v1",
spki_der, "", 32) using the same KDF as id. When present with a
Base64 value, Decryptors match it against known sender public keys.
The special value anon indicates anonymous sender Auth mode: the
sender's key is not identified, and Decryptors perform trial
decryption across candidate sender keys. Mutually exclusive with
shint.¶
A 4-digit decimal value (0000-9999) assigned by the sender
out-of-band; parallels hint for recipient keys. When present,
Decryptors filter candidate sender keys to those associated with
this value. Mutually exclusive with sid.¶
Encryptors MUST include exactly one of sid or shint (but not both)
when using Auth mode.¶
Auth mode token forms extend the base forms:¶
hpke(kem=x25519, kemct=<B64>, id=<B64>, sid=<B64>) hpke(kem=x25519, kemct=<B64>, sid=<B64>) hpke(kem=x25519, kemct=<B64>, sid=anon) hpke(kem=x25519, kemct=<B64>, shint=1234)¶
All combinations of recipient identification (id, hint, or anonymous) and sender identification (sid, shint, or sid=anon) are valid.¶
The hpke step descriptor refines the step-descriptor grammar in Section 6.2.1.1. OWS (optional whitespace) is permitted after each comma separator, consistent with step-params:¶
hpke-step = "hpke(" hpke-params ")"
hpke-params = "kem=" kem-name "," OWS "kemct=" kemct
[ "," OWS recipient-id ]
[ "," OWS sender-id ]
kem-name = 1*( ALPHA / DIGIT / "-" )
; registered in SAFE KEM Identifiers
; registry ({{iana-kem}})
kemct = 1*BASE64CHAR
recipient-id = "id=" 1*BASE64CHAR / "hint=" hint-value
sender-id = "sid=" ( 1*BASE64CHAR / "anon" )
/ "shint=" hint-value
hint-value = 4DIGIT
¶
Encryptors MUST NOT include both id and hint, and MUST NOT include
both sid and shint.¶
Each HPKE step uses HPKE [RFC9180] in export-only mode with ciphersuite (KEM_ID, KDF_ID, 0xFFFF) constructed from the KEM's registered identifiers (Section 5.6.3.2). AEAD ID 0xFFFF disables Seal/Open; only Export is used.¶
For Base mode (default):¶
;; Encryptor (kemct, ctx) = SetupBaseS(pkR, info="SAFE-v1") step_secret = ctx.Export(exporter_context, L=32) ;; Decryptor ctx = SetupBaseR(kemct, skR, info="SAFE-v1") step_secret = ctx.Export(exporter_context, L=32)¶
For Auth mode (sid or shint present):¶
;; Encryptor (kemct, ctx) = SetupAuthS(pkR, skS, info="SAFE-v1") step_secret = ctx.Export(exporter_context, L=32) ;; Decryptor ctx = SetupAuthR(kemct, skR, pkS, info="SAFE-v1") step_secret = ctx.Export(exporter_context, L=32)¶
The kemct value is the KEM ciphertext (enc in HPKE terminology).
The exporter_context is defined in Section 5.6.3.4.¶
This design uses HPKE's standardized key schedule and export interface for KDF agility, while SAFE's own SafeDerive function handles KEK derivation, payload key derivation, and nonce constructions.¶
The following table lists the KEMs defined in the IANA HPKE KEM Identifiers registry [RFC9180] that are recognized by SAFE:¶
| KEM | KEM ID | KDF ID | HPKE Ciphersuite | Key Encoding |
|---|---|---|---|---|
| x25519 | 0x0020 | (see below) | (0x0020, KDF_ID, 0xFFFF) | [RFC8410] |
| p-256 | 0x0010 | (see below) | (0x0010, KDF_ID, 0xFFFF) | [RFC5480] |
| ml-kem-768 | 0x0041 | (see below) | (0x0041, KDF_ID, 0xFFFF) | (see below) |
The HPKE Ciphersuite column shows the (KEM_ID, KDF_ID, AEAD_ID) triple used with HPKE's Setup functions. AEAD ID 0xFFFF selects export-only mode per Section 5.3 of [RFC9180].¶
All KEMs, including DHKEM-based KEMs (x25519, p-256), use the configured KDF, both for the HPKE key schedule and for DHKEM's internal operations per [I-D.ietf-hpke-hpke]: HKDF-SHA256 (KDF ID 0x0001) when KDF=hkdf-sha-256, or TurboSHAKE256 (KDF ID 0x0013) when KDF=turboshake256.¶
When KDF=turboshake256, the HPKE implementation MUST conform to the one-stage key schedule defined in [I-D.ietf-hpke-hpke].¶
ML-KEM-768 key encoding follows [I-D.ietf-lamps-kyber-certificates]. Auth mode requires AuthEncap/AuthDecap, which are defined only for DHKEM-based KEMs. ML-KEM-768 MUST NOT be used with Auth mode. Additional KEMs from the IANA HPKE KEM Identifiers registry MAY be supported following the process defined in Section 10.2.¶
The id parameter in hpke(...) steps identifies the intended recipient
public key. Key identifiers hash the SubjectPublicKeyInfo (SPKI)
Distinguished Encoding Rules (DER) encoding rather than raw key octets.
This ensures key identifiers are consistent with certificate fingerprint
practices and include the algorithm Object Identifier (OID), preventing
collisions between keys of different types.¶
spki_der = DER-encode SubjectPublicKeyInfo for pk
per the KEM's registered SPKI Encoding ({{iana-kem}})
DER encoding MUST be canonical.
id = Base64( SafeDerive("SAFE-SPKI-v1",
spki_der, "", 32) ) per {{RFC4648}}
¶
The resulting Base64 string is the value of the id parameter (44
characters for the 32-octet output).¶
When the step sequence includes one or more hpke(...) steps, the LOCK
MUST include a corresponding kemct parameter value for each HPKE step,
in the same order as they appear in the step sequence. Encryptors MUST
generate a fresh encapsulation per LOCK; reusing a prior encapsulation
is prohibited.¶
For Auth mode, the Decryptor resolves the sender public key as follows:¶
If sid is present and is not anon: match against known
sender public keys using the configured KDF.¶
If shint is present: filter candidate sender keys by the
hint value.¶
If sid=anon: try all locally known sender keys
matching the kem type.¶
The step secret is derived via HPKE's Export interface:¶
exporter_context = SafeDerive("SAFE-STEP",
step_binding, "", 32)
step_secret = ctx.Export(exporter_context, L=32)
¶
where ctx is the HPKE context returned by SetupBaseS/R (or
SetupAuthS/R for Auth mode) as described in Section 5.6.3.2, and
step_binding is the binding form defined in Section 5.6.¶
When id or sid is omitted from the on-wire token, the Decryptor
reconstructs it during trial decryption per Section 5.6.¶
The KEM binds the shared secret to the recipient key (and for Auth mode, the sender key). The exporter_context binds the step secret to the step_binding, preventing key-confusion attacks where an attacker substitutes one recipient's encapsulation for another's. The HPKE info parameter "SAFE-v1" binds the key schedule to this protocol, preventing cross-protocol reuse of the same KEM keys from producing valid SAFE step secrets. Suite binding is not needed here because the final KEK derivation commits to encryption_parameters (Section 5.7.1).¶
Encode form:¶
Encode("hpke", kem, kemct, id)
Encode("hpke", kem, kemct, id,
"auth", sid) ; auth
¶
step_binding: Encode("hpke", kem, kemct, id) for Base mode; Encode("hpke", kem, kemct, id, "auth", sid) for Auth mode. Display-only fields (hint, shint) are not included. The id and sid fields are reconstructed per Section 5.6 when omitted on-wire.¶
SAFE uses two applications of SafeDerive (Section 5.4). The KEK derivation, specified in this section, produces a LOCK-specific KEK from its ordered step secrets. The payload derivation (Section 5.8) produces the per-file payload key, commitment, snapshot authenticator key, and (in derived mode) nonce base from the CEK, encryption parameters, and per-file salt.¶
The following diagram shows the two independent chains:¶
Payload encryption is performed once under the CEK and per-file salt and does not depend on lock structure. Locks are independent wrappers of the same CEK and can be added or removed without touching payload ciphertext.¶
The LOCK key derivation produces a LOCK's KEK from its ordered step secrets in a single call to the universal combiner (Section 5.4):¶
Algorithm:¶
derived_kek = SafeDerive("lock-kek",
[step_secret_1, ..., step_secret_N],
[...encryption_parameters,
step_binding_1, ..., step_binding_N],
Nk)
¶
The ordered step secrets form the ikm argument to SafeDerive, and the encryption parameters followed by the ordered step bindings form the info argument. The combiner frames each as one element (Section 5.4.1). A LOCK is the AND of its steps: every step secret and step binding is bound in this one derivation, and the encryption_parameters commit it to the negotiated AEAD, segment size, KDF, snap_id, and epoch length. Step order is fixed by position in the ikm and info arrays, so steps cannot be reordered. Appendix G.6 gives worked vectors for step-order binding, display-only-field exclusion, and bound-field sensitivity.¶
Within a LOCK, all Steps are required: derived_kek depends on every step secret, so the holder must satisfy every Step (a logical AND). A file MAY carry several LOCKs as independent alternatives; satisfying any one LOCK yields the CEK (a logical OR). Each LOCK wraps the same CEK (Section 5.7.2), so LOCKs can be added or removed without re-encrypting the payload.¶
The LOCK KDF is a single SafeDerive call. The SAFE KDF selected by the CONFIG KDF parameter (Section 5.4) determines how it is evaluated: a two-stage KDF (HKDF-SHA-256) runs Extract then Expand, while a single-stage KDF (TurboSHAKE256) runs Derive. The binding inputs and the secret inputs are identical in either case, so the KDF is a swappable component.¶
Consider a LOCK block with two steps requiring both a passphrase and a private key:¶
Step: pass(kdf=argon2id, salt=<Base64>) Step: hpke(kem=p-256, id=<Base64>, kemct=<Base64>) Encrypted-CEK: <Base64>¶
Evaluation proceeds per Section 5.7.1:¶
// Step 1: passphrase
step_secret_1 = Argon2id(passphrase, salt)
// Step 2: HPKE (export-only mode)
ctx = SetupBaseR(kemct, sk, info="SAFE-v1")
step_secret_2 = ctx.Export(
exporter_context =
SafeDerive("SAFE-STEP",
step_binding_2, "", 32),
L = 32)
// One combiner call over both steps, in order
derived_kek = SafeDerive("lock-kek",
[step_secret_1, step_secret_2],
[...encryption_parameters,
step_binding_1, step_binding_2],
Nk)
¶
The derived_kek depends on both factors. Each step is bound to its position in the ikm and info arrays, preventing step reordering.¶
The per-step salt and kemct values MUST be unique per LOCK. Reusing
these values with the same credentials produces the same step_secret,
weakening KEK uniqueness.¶
With derived_kek computed per Section 5.7.1, the Encrypted-CEK field is:¶
Encrypted-CEK = lock_nonce || AEAD.Seal(
key = derived_kek,
nonce = lock_nonce,
aad = "",
pt = CEK )
¶
The AEAD algorithm is the one specified in the CONFIG block (or the default aes-256-gcm when CONFIG is absent); the same AEAD is used for both CEK wrapping and payload encryption. Empty AAD is sufficient because derived_kek already binds the encryption_parameters and all step_bindings in its single derivation (Section 5.7.1), so repeating this binding in the AEAD AAD would be redundant.¶
Encryptors MUST generate lock_nonce using SafeRandom(Nn,
"SAFE-LOCK-NONCE"); each LOCK requires a fresh value.¶
This section specifies the payload layer: the SEAL named instantiation SAFE adopts and its bindings, the payload key schedule, per-segment nonces, rewrite rules, the snapshot authenticator, and the segment AAD.¶
SAFE's payload layer is SEAL, the construction for building random-access AEAD (raAE) schemes ([I-D.sullivan-cfrg-raae]). The Profile CONFIG field (Section 6.1) names one of the SEAL named instantiations in Table 11 (Section 4.12 of [I-D.sullivan-cfrg-raae]), by that instantiation's own name. Each instantiation fixes the SEAL profile (SEAL-RW-v1 mutable or SEAL-RO-v1 immutable), the segment size, the nonce mode, the snapshot authenticator, the epoch length, and the on-disk layout; the AEAD and KDF CONFIG fields supply the cipher suite. The payload layer derives in the selected SEAL domain, so that instantiation's SEAL vectors apply to it directly. The SAFE key layer (Section 5.7.1) remains in the SAFE-v1 domain. SAFE supplies no global associated data, so SEAL's G input is the empty octet string for every SAFE object. G is always the last element of the commitment info, one zero-length element, so every commitment derivation includes it (Section 4.5.1 of [I-D.sullivan-cfrg-raae]). The payload key, snap key, and nonce base never take G.¶
Implementations MUST implement the payload layer as the selected SEAL named instantiation of [I-D.sullivan-cfrg-raae], using the SEAL parameter bindings given later in this section.¶
| Profile | protocol_id | Segment | Nonce | Snapshot | Epoch | Layout |
|---|---|---|---|---|---|---|
| SEAL-simple | SEAL-RO-v1 | 65536 | derived | 0x0000 | 32 | linear |
| SEAL-attachment | SEAL-RO-v1 | 65536 | derived | 0x0003 | 10 | aligned |
| SEAL-attachment-small | SEAL-RO-v1 | 65536 | derived | 0x0002 | 10 | aligned |
| SEAL-editable | SEAL-RW-v1 | 65536 | random | 0x0001 | 16 | linear |
| SEAL-memory | SEAL-RW-v1 | 16384 | random | 0x0001 | 16 | aligned |
| SEAL-compact | SEAL-RW-v1 | 16384 | derived | 0x0001 | 16 | aligned |
The parameter columns above are informative. Section 4.12 of [I-D.sullivan-cfrg-raae] is authoritative for every value in them, including its treatment of the 256-bit-nonce suites, which resolve the epoch independently of the row. An implementation MUST take these values from that document. Only the Profile column, the CONFIG token, is SAFE's.¶
A row applies its cipher suite to its instantiation, written SEAL-simple(aead_id, kdf_id) and so on (Section 4.12 of [I-D.sullivan-cfrg-raae]). SAFE adopts each instantiation's pinned epoch length rather than the cipher suite's own default (Section 5.9.6 of [I-D.sullivan-cfrg-raae]): the write-once SEAL-simple instantiation takes 32, the write-once SEAL-attachment takes 10 for the epoch digest tree's grouping and SEAL-attachment-small takes the same for its aligned per-segment use case, and the three mutable instantiations take a conservative 16. Those are decimal values, which epoch_length_u8 encodes as I2OSP(r, 1) (Section 5.5), so an epoch length of 10 is the octet 0x0A. Section 4.12 of [I-D.sullivan-cfrg-raae] resolves the epoch in every case, including for the 256-bit-nonce suites, which it treats independently of the row. SAFE does not exercise the epoch override that Section 4.12 of [I-D.sullivan-cfrg-raae] permits. SAFE registers six of raAE's seven instantiations. It does not register SEAL-disk, whose split layout stores two independently growing streams that do not map onto the single-file SAFE format. A future revision may add it.¶
| SEAL parameter | SAFE binding |
|---|---|
| protocol_id | "SEAL-RW-v1" (mutable) or "SEAL-RO-v1" (immutable), per the Profile (Section 6.1) |
| aad_label | "SEAL-DATA" |
| aead_id | uint16 code point of the effective AEAD (Section 10.1; e.g. 0x0002 for aes-256-gcm) |
| segment_max | the effective Segment-Size (16384 or 65536), encoded as uint32 |
| kdf_id | uint16 code point of the effective KDF (Section 10.3; 0x0001 HKDF-SHA-256, 0x0013 TurboSHAKE-256) |
| snap_id | snapshot authenticator, per the Profile (Section 5.8.1): 0x0000 none, or an authenticator registered in Section 4.7 of [I-D.sullivan-cfrg-raae] |
| nonce_mode | fixed by the Profile (Section 6.1): 0x01 derived, 0x00 random |
| epoch_length | the epoch the selected SEAL named instantiation resolves to (Section 5.8.1) |
| CEK | output of the SAFE key layer (Section 5.7.1) |
| salt | 32 octets, SafeRandom(32, "SAFE-SALT") |
| Nh | the KDF output length (Section 10.3) |
| payload_info | the seven-element list defined in Section 5.8.2 |
Each named instantiation is a valid (profile, nonce_mode, snap_id) combination by construction, so no separate tuple-validity check is needed; a Decryptor MUST reject any object whose Profile is not one SAFE defines (Section 6.1).¶
The mutable instantiations (SEAL-editable, SEAL-memory, SEAL-compact) support rewrite, extend, and truncate; SEAL-compact uses a derived nonce and so requires an MRAE AEAD, because rewrites reuse the derived nonce. The immutable instantiations (SEAL-simple, SEAL-attachment, SEAL-attachment-small) follow a write-once discipline and admit any AEAD. Every instantiation whose snap_id is not 0x0000 carries the snapshot authenticator (Section 5.8.7); SEAL-simple is the only one that does not, so it alone provides per-segment confidentiality, integrity, and key commitment without whole-object integrity. Here "immutable" names the writer's write-once discipline, not a tamper-evidence guarantee; an immutable object MUST NOT be rewritten, extended, or truncated.¶
Whole-object integrity, when required, comes from any instantiation whose snap_id is not 0x0000 (Section 5.8.1) or from a layer above SAFE.¶
Every instantiation provides per-segment confidentiality, per-segment integrity (binding index and finality), and key commitment. Every instantiation except SEAL-simple adds snapshot (whole-object) integrity. The full property statement is the Operational Summary (Section 5.1) of [I-D.sullivan-cfrg-raae].¶
No profile provides rollback protection: replaying an earlier valid snapshot or whole object is not detected, so freshness must be supplied by an external mechanism such as a monotonic version counter.¶
The payload schedule is the SEAL payload schedule (Section 4.5.1 of [I-D.sullivan-cfrg-raae], Payload Schedule) with the bindings of Section 5.8.1. SAFE's payload_info is the SEAL seven-element list (Section 4.4.1 of [I-D.sullivan-cfrg-raae], Payload Info Construction), in this exact order:¶
payload_info = [aead_id, segment_max_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt]
¶
Section 4.4.1 of [I-D.sullivan-cfrg-raae] gives each element's encoding. SAFE's values for them are the bindings of Section 5.8.1, which fixes protocol_id, snap_id, and the nonce mode from the Profile (Section 6.1).¶
payload_info is the SEAL payload_info verbatim. encryption_parameters (Section 5.5) is the five-element SAFE key-layer binding [aead_id, segment_size_be, kdf_id, snap_id, epoch_length_u8]. payload_info adds nonce_mode, between snap_id and epoch_length_u8, and the salt, so the two lists diverge at nonce_mode.¶
The encryption_parameters element segment_size_be and the payload_info element segment_max_be name the same value, the effective Segment-Size, in the SAFE key layer and the SEAL payload respectively.¶
The salt is stored with the payload, leading the raAE header (Section 6.4), so that even when the same CEK is reused across files each file produces unique schedule outputs.¶
The commitment is commitment_length octets, where commitment_length is Nh, the KDF output length (Section 10.3).¶
Before deriving or using any segment key, a Decryptor MUST re-derive the commitment from its own recovered CEK and its resolved payload_info and MUST reject the object if the derived commitment does not match the stored value (see Section 8.13).¶
The SAFE key layer binds snap_id and epoch_length_u8 through encryption_parameters (Section 5.5), but not protocol_id or nonce_mode. For those two the commitment check is the sole assurance that the Decryptor's resolved parameters match the ones the object was created under. SEAL-memory and SEAL-compact are the pair that turns on it: they agree on every encryption_parameters element and differ only at nonce_mode, so under a misuse-resistant AEAD, where both are legal, the commitment alone separates them, and it rejects before any plaintext is released. Any other confusion fails earlier, at the Encrypted-CEK unwrap: a wrong AEAD or KDF changes aead_id or kdf_id, and any other pair of Profiles differs in an element that encryption_parameters binds.¶
The following table summarizes the lifecycle of each payload-layer value: its type and size, whether it is secret, whether it appears on the wire, and when it is computed. The length symbols are:¶
KDF output length (32 for HKDF-SHA-256, 64 for TurboSHAKE-256).¶
AEAD key length.¶
AEAD nonce length.¶
the snapshot value length for the resolved snap_id, and 0 when snap_id is 0x0000 (Section 4.7.1 of [I-D.sullivan-cfrg-raae]).¶
The snapshot value is present whenever the Profile's snap_id is not 0x0000 (Section 5.8.1). A Profile's snapshot and its write discipline are independent, and Section 4.10.2 of [I-D.sullivan-cfrg-raae] gives which authenticators each SEAL profile admits. Per-segment nonces are stored only in random mode (nonce_mode 0x00). In derived mode (nonce_mode 0x01) the nonce is recomputed and never stored.¶
| Value | Type / size | Secret? | On wire? | When computed / lifetime |
|---|---|---|---|---|
| CEK | AEAD key, Nk octets | Yes | No (wrapped per recipient as Encrypted-CEK in each LOCK) | Sampled once per file by the Encryptor; recovered by a Decryptor via the KEK layer (Section 5.7.1); zeroized after use |
| salt | 32 octets, SafeRandom(32, "SAFE-SALT") | No | Yes (leads the raAE header, Section 6.4) | Sampled once per file; reused on append and file-extension; binds every payload schedule output to this file |
| commitment | Nh octets, SealDerive("commit", CEK, [...payload_info, G], Nh) | No | Yes (immediately after salt) | Derived once from CEK and payload_info with the empty G (Section 5.8.1); a Decryptor re-derives and MUST match it before any segment key derivation (Section 8.13) |
| payload_key | Nk octets, SealDerive("payload_key", CEK, payload_info, Nk) | Yes | No | Derived once per file from CEK; root of all epoch and segment keys; never used directly as a segment key; zeroized after use |
| snap_key | Nh octets, SealDerive("acc_key", CEK, payload_info, Nh) | Yes | No | Derived once per file from CEK when snap_id is not 0x0000; the snapshot authenticator's key (Section 5.8.7); zeroized after use |
| epoch_key(j) | Nk octets, SealDerive("epoch_key", payload_key, [uint64(j)], Nk) | Yes | No | Derived per epoch j = i >> r from payload_key; one epoch spans 2^r segments; SHOULD be cached across segments in the same epoch |
| segment_key(i) | Nk octets, segment_key(i) = epoch_key(i >> r) | Yes | No | The epoch key for segment i; computed per segment at encrypt/decrypt time (Section 5.8.3) |
| per-segment nonce (nonce_i) | Nn octets | No | Conditional: stored only in random mode (nonce_mode 0x00); recomputed, not stored, in derived mode (nonce_mode 0x01) | Random mode: sampled per segment (and resampled on rewrite). Derived mode: nonce_i derived from nonce_base, segment index, and finality bit (Section 5.8.4) |
| nonce_base | Nn octets, SealDerive("nonce_base", CEK, payload_info, Nn) | No | No | Present only for derived nonce mode (nonce_mode 0x01); per-segment nonces are derived from it and the segment index |
| snapshot value | Na octets (opaque), produced by the snapshot authenticator (Section 5.8.7) | No | Yes (after the commitment, unless snap_id is 0x0000) | Produced and updated by the snapshot authenticator from snap_key and each segment's per-segment input (Section 5.8.7); verified via SnapVerify before release; absent when snap_id is 0x0000 |
| ciphertext digest | Nh octets per segment, LH(ct_i) (Section 4.7.5 of [I-D.sullivan-cfrg-raae]) | No | Yes, inside the segment's metadata entry, under snap_id 0x0002 and 0x0003 only | Computed per segment from its ciphertext; with the segment's tag, which every entry stores, it forms that segment's leaf, the authenticator's per-segment input at those snap_ids (Section 5.8.7) |
| epoch heads | E * Nh octets, where E is the epoch count (Section 6.4.2) | No | Yes, in the aligned header, under snap_id 0x0003 only | Produced by the epoch digest tree, one head per epoch (Section 4.7.6 of [I-D.sullivan-cfrg-raae]) |
Per-segment encryption uses an epoch key derived from the payload key under the selected SEAL profile, following Section 4.5.2 (Epoch Key Derivation) of [I-D.sullivan-cfrg-raae]: each segment uses the epoch key for its epoch, and the payload key is never used directly as a segment key. The epoch length r is the one the selected SEAL named instantiation resolves to (Section 4.12 of [I-D.sullivan-cfrg-raae]), including that document's treatment of the 256-bit-nonce suites.¶
Since the epoch length is part of payload_info (Section 5.8.2), the commitment and all payload schedule outputs change when the epoch configuration changes. Implementations SHOULD cache epoch keys across consecutive segments in the same epoch. [FLOE] provides the security definitions and proofs.¶
Per-segment AEAD encryption follows the SEAL encryption procedure (Section 4.8 of [I-D.sullivan-cfrg-raae], Segment Algorithms), using segment_key(i) (Section 5.8.3) and the segment AAD (Section 5.8.8). The nonce constructions are SEAL's (Section 4.5.3 of [I-D.sullivan-cfrg-raae], Nonce Generation). Tag size is 16 octets per [RFC5116]; implementations receiving a single AEAD output MUST split at offset len(output) - 16. SAFE resolves the nonce mode from the Profile (Section 5.8.1): random (0x00) or derived (0x01). The resolved mode governs what SAFE stores per segment.¶
In random mode (nonce_mode 0x00), Encryptors MUST use one of the nonce constructions in Section 5.8.5; each segment's nonce and tag are stored:¶
segment_metadata_i = nonce_i || tag_i (Nn + 16 octets)¶
In the linear layout, each encrypted segment is stored as nonce_i ||
ciphertext_i || tag_i. In the aligned layout, segment
metadata is stored apart from the ciphertext (Section 6.4),
enabling single-seek segment access.¶
In derived mode (nonce_mode 0x01), each segment's nonce is the SEAL derived nonce (Section 4.5.3.2 of [I-D.sullivan-cfrg-raae], Derived Nonce Mode), recomputed from the key schedule rather than stored. What remains depends on the resolved snap_id:¶
segment_metadata_i = tag_i (16 octets)
LH(ct_i) || tag_i (Nh + 16 octets,
snap_id 0x0002, 0x0003)
¶
Under snap_id 0x0002 and 0x0003 the entry is the segment's leaf, which the authenticator consumes (Section 4.11.2 of [I-D.sullivan-cfrg-raae]).¶
See Section 8.10 and Section 8.10.2 for security rationale. This design enables re-encryption of the payload without re-wrapping the CEK for each recipient, and supports selective editing of individual segments.¶
In random mode (nonce_mode 0x00), SAFE uses the SEAL nonce constructions
defined in Section 4.5.3 of [I-D.sullivan-cfrg-raae], Nonce
Generation, sourcing the fresh randomness from SafeRandom(Nn,
"SAFE-NONCE"). These produce Nn-octet nonces. Nonce derivation is
one-pass and supports parallel segment encryption. Decryptors read the
stored nonce from each segment and do not need to know which
construction was used.¶
Encryptors SHOULD incorporate plaintext into per-segment nonce
derivation for SIV-like misuse resistance when RNG state duplication is
a concern (e.g., VM snapshots, process forks without reseed). This
construction is specified in Appendix A of [I-D.sullivan-cfrg-raae];
SAFE sources the fresh randomness from SafeRandom(Nn, "SAFE-NONCE").
Under RNG state duplication, it produces different nonces when plaintext
differs, limiting exposure to leaking equality of identical segments at
the same index.¶
Segment rewrite follows the SEAL rewrite procedure (Section 4.8.3 of
[I-D.sullivan-cfrg-raae], RewriteSegment), updating the snapshot
authenticator as in Section 5.8.7. The rewrite nonce
follows the resolved nonce mode (Section 5.8.1), not the AEAD. In
random mode (nonce_mode 0x00) the Encryptor MUST sample a fresh nonce_i
= SafeRandom(Nn, "SAFE-NONCE") and update the stored nonce alongside
the new tag. In derived mode (nonce_mode 0x01) the nonce for that (i,
is_final) is fixed by the segment index and is reused on rewrite, which
is why the derived-nonce mutable instantiation requires an MRAE AEAD
(Section 5.8.1): MRAE tolerates that reuse, leaking equality rather
than content.¶
A derived-mode rewrite is therefore deterministic: identical plaintext at the same index produces identical ciphertext. See Section 8.10.2.¶
When the Profile's snap_id is not 0x0000, the object carries the SEAL snapshot authenticator that snap_id names (Section 4.7 of [I-D.sullivan-cfrg-raae]). At 0x0000 there is none, and the object provides per-segment authenticity but not whole-object integrity. The snapshot is independent of the write discipline: an immutable Profile may carry one (Section 5.8.1).¶
The snapshot value is an opaque field of Na octets, present only when snap_id is not 0x0000. SAFE stores it in the DATA block and supplies the authenticator with its key (from the payload schedule) and each segment's per-segment input, which the authenticator defines: the segment's AEAD tag at snap_id 0x0001, and its leaf, LH(ct_i) || tag_i, at 0x0002 and 0x0003 (Section 4.7.1 of [I-D.sullivan-cfrg-raae]). Its size, construction, update, and the integrity it provides are the authenticator's, specified in Section 4.7 of [I-D.sullivan-cfrg-raae].¶
Before releasing any plaintext, a Decryptor MUST run the authenticator's SnapVerify over the present segment set and the stored snapshot value, using a constant-time comparison, and MUST treat a verification failure as an authentication failure for the object.¶
How a Decryptor covers a single segment depends on the authenticator. Under snap_id 0x0001 the snapshot value is computed over every segment's input, so it is verifiable only over the whole object: a Decryptor that processes segments incrementally MUST complete SnapVerify once all segments have been read, and until it succeeds MUST NOT release decrypted plaintext to the caller unless it documents the weaker guarantee, that early-released plaintext carries per-segment authenticity but not snapshot integrity (Section 6.6 of [I-D.sullivan-cfrg-raae], Snapshot Integrity Limitations).¶
Under snap_id 0x0002 and 0x0003 a Decryptor MAY instead cover one segment on its own, which is what the write-once aligned Profiles are for (Section 5.8.1). It authenticates the snapshot from the header, without reading any ciphertext, and then follows that authenticator's own per-segment procedure, which differs between the two (Sections 4.7.5 and 4.7.6 of [I-D.sullivan-cfrg-raae]). A Decryptor MUST authenticate the snapshot before it relies on any stored leaf: a leaf that has not been authenticated is attacker-supplied, and comparing against it establishes nothing. A segment covered this way carries snapshot integrity without a read of the whole object.¶
The segment AAD is the SEAL segment_aad construction (Section 4.4.2 of [I-D.sullivan-cfrg-raae], Segment AAD) with aad_label = "SEAL-DATA". Whether the segment index and the finality bit are bound in the AAD or in the nonce follows the resolved nonce mode, which that section defines. SAFE supplies an empty per-segment associated data A_i.¶
The is_final flag provides truncation detection: if a Decryptor decrypts segment i with is_final=0 and no subsequent segment exists, truncation has occurred. Decryptors MUST abort when truncation is detected (is_final=0 with no successor segment). For random-access reads, Decryptors SHOULD verify that the segment count N in the metadata matches the actual number of segments. The flag also prevents extension attacks: appending segments after a segment marked is_final=1 will fail AEAD verification.¶
For streaming writes where the total segment count is unknown, Encryptors buffer the last segment until more data arrives or the stream ends. All emitted segments use is_final=0; only when the stream closes does the Encryptor encrypt the final segment with is_final=1.¶
Encryptors MUST ensure segment indices remain below 2^64 in random mode (nonce_mode 0x00) and below 2^63 in derived mode (nonce_mode 0x01), where the segment index and finality bit are packed into the low 8 octets of the derived nonce (Section 4.5.3.2 of [I-D.sullivan-cfrg-raae]). Encryptors SHOULD limit i to at most 2^48 to avoid Base64 strings exceeding typical filesystem or object store limits; this is a practical recommendation, not a protocol limit. Decryptors MUST reject segment indices i where i >= 2^64 in random mode, or i >= 2^63 in derived mode.¶
A SAFE encoding MUST consist of an optional config header (Section 6.1), followed by one or more LOCK blocks (Section 6.2), followed by exactly one DATA block (Section 6.4). Blocks MUST appear in this order; Decryptors MUST reject encodings that violate it. There is no version marker in the fences, and binary encoding carries the DATA payload without them (Section 6.3.2). Multiple LOCK blocks provide multi-recipient encryption; the DATA block is shared.¶
The config header may be omitted when all defaults apply. When present, it lists only non-default parameters. A Decryptor parses the config to determine the KDF before computing key identifiers for LOCK selection (Section 6.2.3); when the config is absent, the Decryptor uses the default KDF (hkdf-sha-256).¶
The header is:¶
-----BEGIN SAFE CONFIG-----
Profile: SEAL-simple | SEAL-attachment | SEAL-attachment-small | SEAL-editable
| SEAL-memory | SEAL-compact
AEAD: aes-256-gcm | chacha20-poly1305 | ...
KDF: hkdf-sha-256 | turboshake256
Lock-Encoding: armored | readable
Data-Encoding: armored | binary
-----END SAFE CONFIG-----
¶
All CONFIG fields are optional. Omitted fields use default values (Section 5.1): Profile defaults to SEAL-editable, AEAD to aes-256-gcm, KDF to hkdf-sha-256, Lock-Encoding to armored, and Data-Encoding to binary.¶
Profile selects one of the SEAL named instantiations (Section 4.12 of [I-D.sullivan-cfrg-raae]) by that instantiation's own name: SEAL-simple, SEAL-attachment, SEAL-attachment-small, SEAL-editable, SEAL-memory, or SEAL-compact. Each instantiation fixes the SEAL profile, the segment size, the nonce mode, the snapshot authenticator (snap_id), the epoch length, and the on-disk layout; their resolved parameters and SAFE bindings are given in Section 5.8.1. AEAD and KDF choose the cipher suite the instantiation is applied to, written SEAL-simple(aead_id, kdf_id) and so on.¶
CONFIG carries two kinds of options:¶
Profile, AEAD, and KDF determine the five elements of encryption_parameters (Section 5.5) and are folded into the KEK (Section 5.7.1), so changing any of them changes the keys. A Decryptor confirms them through the commitment (Section 5.8.2).¶
Lock-Encoding and Data-Encoding are presentational and bind nothing. Lock-Encoding specifies the LOCK block representation (Section 6.2). Data-Encoding specifies the payload format (Section 6.3): "armored" uses Base64 within DATA fence markers, and "binary" uses raw binary after the last LOCK, in the layout the Profile fixes.¶
The Section 10.5 registry records each option's scope.¶
The epoch length is pinned by the selected SEAL named instantiation (Section 5.8.1) rather than chosen in CONFIG. The 256-bit-nonce suites are the one case the instantiation does not settle on its own: Section 4.12 of [I-D.sullivan-cfrg-raae] gives them a flat key, whose 256-bit nonce makes per-key collision bounds operationally irrelevant (Section 8.14).¶
Implementations MUST support Lock-Encoding: armored and Data-Encoding: binary. Support for Lock-Encoding: readable and Data-Encoding: armored is OPTIONAL.¶
A conforming SAFE file MAY omit the SAFE CONFIG block entirely; parsers MUST treat this identically to a CONFIG block with all defaults. When CONFIG is present, it MAY contain any subset of fields. Implementations MUST construct encryption_parameters using defaults for any omitted fields, including the Profile default that fixes the segment size and snap_id.¶
Field names within the SAFE CONFIG are case-sensitive. Encryptors MUST NOT include duplicate field names; Decryptors MUST reject SAFE CONFIG blocks containing duplicate fields. Decryptors MUST reject SAFE CONFIG blocks containing field names not listed in the SAFE Config Options registry (Section 10.5), and MUST reject a Profile value that is not SEAL-simple, SEAL-attachment, SEAL-attachment-small, SEAL-editable, SEAL-memory, or SEAL-compact. Decryptors MUST also reject a CONFIG that selects the SEAL-compact Profile with a non-MRAE AEAD (Section 5.2.1); that instantiation requires misuse resistance (Section 5.8.1).¶
The order of fields within a CONFIG block is not significant. Encryptors MAY emit fields in any order; Decryptors MUST accept fields in any order.¶
All header lines MUST contain only ASCII characters (octets 0x20-0x7E and LF). Encryptors MUST NOT include non-ASCII characters in field names or values. Decryptors MUST reject SAFE CONFIG blocks containing non-ASCII octets or malformed UTF-8 sequences.¶
Implementations SHOULD bound SAFE CONFIG size; Decryptors MAY reject SAFE CONFIG headers exceeding 64 KiB.¶
Field values MAY wrap across multiple lines using the same rules as LOCK blocks (Section 6.2.1.2): continuation lines MUST be indented with at least two spaces, and Decryptors MUST concatenate continuation lines (stripping leading whitespace) before processing.¶
A LOCK block defines one independently satisfiable access path to the CEK and carries the artifacts needed to recover it. Each LOCK contains one or more steps and exactly one Encrypted-CEK. An access path is typically a single recipient, but the abstraction is broader: a LOCK can encode a role, a group, or a recovery path, and one recipient MAY hold the credentials for several LOCKs.¶
The steps within a LOCK are conjunctive (AND): every step MUST be satisfied to derive that LOCK's KEK, so a LOCK that requires both a passphrase and a private key admits no single-factor unlock. Multiple LOCKs are disjunctive (OR): any one LOCK whose steps are all satisfied recovers the CEK, which is how several recipients decrypt a shared payload without duplicating ciphertext.¶
Steps are evaluated in the order they appear. Step-specific inputs are
carried as parameters (e.g., salt= for pass, kemct= for hpke). See
Section 5.6.2 and Section 5.6.3 for step-specific requirements.¶
The Encrypted-CEK is the concatenation of lock_nonce and the AEAD
ciphertext of the CEK under the derived KEK with empty associated data
(Section 5.7.2). The lock_nonce length is the AEAD's nonce
size (Nn) as specified in Section 10.1. Encryptors MUST generate a
fresh lock_nonce per LOCK using SafeRandom(Nn, "SAFE-LOCK-NONCE")
(Section 5.4.2). Decryptors MUST reject Encrypted-CEK values
that do not decode to exactly Nn + 32 + 16 octets (lock_nonce, CEK
ciphertext, AEAD tag).¶
Decryptors MUST skip LOCK blocks containing unknown KEM identifiers or unknown step types, and attempt other LOCKs (if available). Decryptors MUST reject step descriptors containing parameter names not defined for the step type in its registration.¶
Implementations SHOULD bound the number of LOCK blocks; Decryptors MAY reject files containing more than 1024 LOCK blocks to prevent resource exhaustion.¶
Two Lock-Encoding values are defined: readable (text) and armored (binary). Both produce the same step_bindings for the KEK schedule (Section 5.6).¶
The readable format uses text step descriptors and colon-delimited fields:¶
-----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=<Base64>) Step: hpke(kem=x25519, id=<Base64>, kemct=<Base64>) Encrypted-CEK: <Base64> -----END SAFE LOCK-----¶
Field names are case-sensitive. Encryptors MUST NOT include fields other than Step and Encrypted-CEK. Encryptors MUST include at least one Step line and exactly one Encrypted-CEK line. Decryptors MUST reject LOCK blocks containing multiple Encrypted-CEK lines or unknown field names. Optional whitespace (OWS) after colons and commas is permitted for readability.¶
Each Step line declares a single cryptographic step. Multiple steps form an ordered sequence with AND semantics: all steps MUST be satisfied to derive the KEK. The syntax follows this ABNF, which applies after Decryptors perform line unfolding (concatenating continuation lines and stripping leading whitespace per Section 6.2.1.2):¶
step-line = "Step:" OWS step-descriptor LF
step-descriptor = step-name "(" step-params ")"
step-name = 1*( ALPHA / DIGIT / "-" )
step-params = param *( "," OWS param )
param = param-name "=" param-value
param-name = 1*( ALPHA / DIGIT / "-" )
param-value = 1*PCHAR
PCHAR = %x21-28 / %x2A-2B / %x2D-7E
; VCHAR except SP, ")", ","
OWS = *( SP / HTAB )
¶
The step_binding used in the KEK schedule (Section 5.7.1) is derived per Section 5.6: extract the binding fields and encode them with Encode().¶
Each Step line contains exactly one step descriptor. LOCK blocks with
multiple steps use multiple Step lines. Step-specific inputs are
carried as step descriptor parameters (e.g., salt= for pass, kemct=
for hpke).¶
The param-value production forbids spaces (SP, 0x20) and tabs (HTAB, 0x09). Percent-encoding is not supported; all parameter values MUST be literal UTF-8 printable characters excluding whitespace.¶
Encryptors MUST emit parameters in the order specified by the step definition. Decryptors MUST reject step descriptors whose parameters are not in the specified order. Decryptors MUST reject step descriptors containing duplicate parameter names within a single step. Decryptors MUST reject a step descriptor that omits a required parameter (for example, a pass step without salt or an hpke step without kemct). A parameter is required when the step's ABNF grammar, given as the Parameters Grammar of its registration (Section 10.4), does not enclose it in an optional group.¶
See Section 5.7 for how step secrets are combined to derive the KEK. See Section 5.6.3 for the HPKE step format and Section 5.6.2 for the passphrase step format.¶
Field values MAY wrap across multiple lines. Continuation lines MUST be indented with at least two spaces. Decryptors MUST unfold wrapped values by concatenating continuation lines and stripping leading whitespace.¶
Step descriptors MAY wrap at parameter boundaries (after commas). Encryptors SHOULD insert a space after the comma at each wrap point. Continuation lines use 4-space indent.¶
Encrypted-CEK values use 2-space indent for continuation lines.¶
Encryptors SHOULD wrap lines at 64 characters. Decryptors MUST accept any line length.¶
A field value extends from immediately after the colon (and any following whitespace) until one of:¶
A line starting with "Step:" (unindented)¶
A line starting with "Encrypted-CEK:" (unindented)¶
A fence line "-----END SAFE LOCK-----"¶
Trailing whitespace on individual lines SHOULD be avoided; Decryptors MUST strip trailing spaces and tabs from each line before concatenation.¶
Example with wrapped HPKE step descriptor:¶
Step: hpke(kem=ml-kem-768, hint=4217,
kemct=bWxrZW03NjhrZW1jaXBoZXJ0ZXh0
ZXh0cmVtZWx5bG9uZ2JhY2U2NGVuY29kZW
RkYXRhYXBwcm94aW1hdGVseTEwODhvY3Rl
dHNmb3JwcXNlY3VyaXR5)
¶
Decryptors parse the text, extract field values, and produce the step Encode form for binding.¶
When Lock-Encoding is armored, the SAFE LOCK block contains a single Base64 value: the Base64 encoding of the LOCK serialized in its readable form (Section 6.2.1), namely the Step and Encrypted-CEK field lines without the BEGIN/END fence lines, with the final field extending to the end of the decoded value. The armored and readable encodings are the same LOCK grammar. Armored only Base64-wraps the readable serialization so the LOCK is ASCII-armored like the rest of the object.¶
To decode, a Decryptor concatenates the Base64 continuation lines (stripping leading whitespace), Base64-decodes them, and parses the result as a readable LOCK (Section 6.2.1). Every readable rule applies unchanged, including the step descriptor grammar, the required parameter order, the Encrypted-CEK field, and reconstruction of each step_binding from the descriptor and the Decryptor's own credentials (Section 5.6). The armored bytes are never an input to any derivation, so the LOCK key derivation (Section 5.7.1) is unchanged and the readable and armored encodings are lossless transcodings of one LOCK.¶
Encryptors MUST use Base64 per the rules in Section 4. The Base64 value MAY wrap across multiple lines; continuation lines MUST be indented with at least two spaces. Decryptors MUST concatenate the continuation lines (stripping leading whitespace) before decoding.¶
A decryptor parses the config header (Section 6.1) to determine the KDF, then computes key identifiers to determine candidate LOCK blocks. This does not require touching the SAFE data block.¶
Resolving a step is uniform across step types: given a step_descriptor and the decryptor's local credentials, a step yields zero or more ordered (step_binding, step_secret) results. It yields none when no credential matches (the LOCK is not a candidate), one when the match is unambiguous, or several when a hinted or anonymous descriptor matches more than one local credential. The candidate selection and trial decryption below produce these results for the built-in steps; the ordered pairs feed, in step order, the single LOCK key derivation (Section 5.7.1). The HPKE Identified, Hinted, and Anonymous modes are cases of this resolution, not separate mechanisms.¶
For each LOCK block, determine candidacy as follows:¶
Parse the Step descriptors. If any descriptor references an unsupported step type, the LOCK is not a candidate.¶
For each hpke(...) step, determine candidate keys based on mode:¶
Identified (id present): : Compute the key identifier for locally
available public keys using SafeDerive("SAFE-SPKI-v1", spki_der, "",
32) with the configured KDF (default: hkdf-sha-256). Keys whose
identifier matches the id parameter are candidates.¶
Hinted (hint present): : Look up keys in local storage associated
with this hint value. Keys with matching hint are candidates.¶
Anonymous (neither id nor hint): : All local keys matching the
kem type are candidates.¶
If no local recipient keys are candidates, the LOCK is not a candidate.¶
For auth-mode hpke(...) steps (sid or shint present), also determine candidate sender keys:¶
Identified (sid with Base64 value): : Keys whose identifier
matches sid are candidates.¶
Hinted (shint present): : Keys with matching shint are candidates.¶
Anonymous (sid=anon): : All locally known sender keys matching the
kem type are candidates.¶
If no sender keys are candidates for an auth-mode step, the LOCK is not a candidate.¶
Among remaining candidates, Decryptors SHOULD attempt LOCKs in order of confidence:¶
LOCKs where all hpke steps are identified or hinted; the decryptor has confirmed it holds matching keys.¶
LOCKs with anonymous hpke steps; requires trial decryption across all keys of the matching KEM type.¶
LOCKs with pass steps; may require user interaction, so defer until key-only LOCKs are exhausted.¶
Encryptors MAY include multiple pass(...)-only LOCK blocks if they use different KDF variants (e.g., one pass(kdf=argon2id, ...) and one pass(kdf=pbkdf2, ...) for the same passphrase). This enables interoperability between implementations with different passphrase KDF support. Encryptors MUST NOT include duplicate pass(...)-only LOCKs with the same KDF variant. Decryptors MUST stop at the first successful CEK recovery. Decryptors MAY attempt multiple candidates in parallel.¶
For hinted or anonymous step sequences, Decryptors iterate through candidate key combinations. For composable step sequences (multiple hpke steps with AND semantics), trial decryption MUST consider the combinatorial product of candidates for each step. For auth-mode steps, the product includes sender key candidates in addition to recipient key candidates. For each combination:¶
Establish an HPKE context for each hpke(...) step via SetupBaseR (or SetupAuthR with the candidate sender key for auth-mode steps) and call Export per Section 5.6.3.4¶
Derive the KEK from the step secrets per Section 5.7.1¶
Attempt to open Encrypted-CEK with the derived KEK¶
A candidate succeeds when AEAD tag verification passes on Encrypted-CEK. If the KEK is wrong, the tag will not verify.¶
Decryptors SHOULD limit the number of LOCKs they attempt to decrypt per credential to a reasonable upper bound. A limit of 1024 LOCKs per credential is RECOMMENDED. Files containing more LOCKs than this bound MAY be partially processed; Decryptors MUST report a clear error when the bound is exceeded rather than silently failing.¶
This bound complements the per-LOCK MaxTrialAttempts limit in Section 8.7.3, which bounds combinatorial key search within a single LOCK. Together they cap total trial-decryption work at 1024 LOCKs x 1024 attempts per LOCK per credential.¶
The Data-Encoding CONFIG field specifies how the payload is represented. The Profile fixes the payload layout (Section 5.8.1), either linear (Section 6.4.1) or aligned (Section 6.4.2). Data-Encoding chooses the representation of that layout. Two encoding values are defined:¶
| Value | Representation |
|---|---|
| armored | Base64 within fence markers (linear Profiles) |
| binary | Raw binary in the Profile's layout |
The default is "binary" when Data-Encoding is omitted. Binary encoding is defined over every layout, so the default is valid for every Profile. Armored encoding is defined only over the linear layout, so it applies only to the Profiles that fix one (Section 5.8.1). A Decryptor MUST reject an object whose Data-Encoding is armored when its Profile fixes an aligned layout.¶
Armored encoding wraps the linear layout (Section 6.4.1) in Base64 with fence markers:¶
-----BEGIN SAFE DATA----- <Base64: linear_payload> -----END SAFE DATA-----¶
The Base64 string MAY be wrapped across multiple lines for readability. When wrapped, each line break MUST be a single LF character. For length calculations and random-access arithmetic, Decryptors MUST first remove all line breaks (LF and CRLF) and CR octets (0x0D), then strip trailing whitespace from the result. The normalized string length determines padding and segment offset computations. Decryptors MUST ignore these characters during Base64 decoding and concatenate all lines before decoding.¶
Encryptors SHOULD wrap Base64 lines at 64 characters. Decryptors MUST accept any line length.¶
Implementations MAY enforce an upper bound on payload size to prevent over-allocation; Decryptors MAY reject payloads exceeding 64 TiB of ciphertext.¶
Armored data arithmetic (computing segment count, byte-to-Base64 offsets, and per-segment decryption) is detailed in Appendix D.¶
Binary encoding omits fence markers. Binary data begins at the octet
immediately following the LF (0x0A) that terminates the last -----END
SAFE ...----- line (typically the final LOCK block). Binary data ends
at EOF.¶
Binary encoding stores the Profile's layout (Section 5.8.1) as raw octets, whether linear (Section 6.4.1) or aligned (Section 6.4.2). The aligned layout is optimized for random access to large files via memory-mapped I/O or O_DIRECT.¶
Implementations that do not support binary encoding MUST fail when
encountering Data-Encoding: binary, consistent with the handling of
unknown AEAD or KDF values.¶
Encryptors that need text-safe output, such as embedding in email or a version-controlled text file, SHOULD set Data-Encoding: armored on a linear-layout Profile (Section 7.4.1). Binary is the default encoding and the only encoding for the aligned-layout Profiles.¶
SAFE uses two payload layouts that describe how encrypted segments are structured. The Profile fixes which layout applies (Section 5.8.1).¶
The linear layout is raAE's (Section 4.11.1, Linear Layout, of [I-D.sullivan-cfrg-raae]): the 32-octet salt, the Nh-octet commitment (Section 5.8.2), the Na-octet snapshot value (Section 5.8.7), and the encrypted segments in index order, each the ciphertext and its tag with the nonce stored in front only in random mode. Under the SEAL-simple Profile there is no snapshot value (Na = 0), the reduced form of Section 4.11.4 of [I-D.sullivan-cfrg-raae]. SAFE pins raAE's streaming rule: all segments except the final one carry Segment-Size octets of plaintext, and the final segment MAY be smaller, down to zero.¶
Zero-length plaintexts are allowed. A zero-length plaintext produces N = 1, L_final = 0. With HKDF-SHA-256 (Nh = 32) and Nn = 12, the minimum linear payload is 156 octets under SEAL-editable (32-octet salt + 32-octet commitment + 64-octet snapshot value + 28-octet encrypted segment) and 80 octets under SEAL-simple, which stores no nonce and no snapshot value (32 + 32 + 16).¶
Decryptors MUST reject payloads with unexpected structure: incorrect commitment length, missing or invalid snapshot value, or segment boundaries that do not align with expected sizes.¶
The snapshot authenticator binds the segment count, which is not stored separately (Section 5.8.7).¶
Streaming readers MUST NOT infer finality from segment size. A full-size segment of exactly C octets (Nn + B + 16 in random mode, nonce_mode 0x00, and B + 16 in derived mode, nonce_mode 0x01) MAY be the final segment, since the final segment is permitted to equal Segment-Size, and a truncated file whose remainder happens to align to a C boundary is indistinguishable from a legitimate file by length alone.¶
The authoritative finality signal is the is_final flag bound to each segment (Section 5.8.8). A reader decrypts segments sequentially, and the segment that decrypts with is_final=1 is the final segment, whatever its size.¶
A reader MUST abort with a truncation error if either of the following holds:¶
it reaches EOF without decrypting a segment marked is_final=1; or¶
any decrypted segment other than the last present segment carries is_final=1 (an in-band final marker followed by trailing octets indicates extension).¶
Having established the segment count N this way, a reader whose Profile carries a snapshot value MUST run SnapVerify over the N segments and that value, and reject the file on mismatch. Under SEAL-simple there is none, and the per-segment binding is the whole of what the object provides (Section 5.8.1).¶
The aligned layout structures the file so that every ciphertext segment begins at an offset that is an exact multiple of the Segment-Size B. This alignment enables efficient memory-mapped I/O and O_DIRECT access, since the operating system can read any segment without copying data across page boundaries.¶
The file begins with a prefix carrying SAFE's own machinery: the text headers (CONFIG and LOCK blocks), then the segment count N and the first ciphertext segment index D. The raAE header follows in the field order of Section 4.11.2 of [I-D.sullivan-cfrg-raae], padded with zeros to a segment boundary, followed by zero or more padding segments for append growth, then the ciphertext segments. raAE reserves the prefix for a consuming protocol's own machinery and fixes the order of the fields that follow it (Section 4.12 of [I-D.sullivan-cfrg-raae]).¶
Let B denote the Segment-Size in octets (16384 or 65536). Let Nn be the nonce size (12 for AES-GCM and ChaCha20, 32 for AEGIS-256 and AEGIS-256X2). N is the segment count (uint32), and D is the first ciphertext segment index (uint32).¶
Two lengths follow the resolved snap_id and nonce mode rather than being fixed, and Section 4.11.2 of [I-D.sullivan-cfrg-raae] defines both. meta_len is the per-segment metadata size: an entry stores the nonce in random mode and recomputes it in derived mode, and under snap_id 0x0002 or 0x0003 it additionally stores that segment's ciphertext digest, which makes the entry Nh octets longer. Na is the snapshot value length for the resolved authenticator (Section 4.7.1 of [I-D.sullivan-cfrg-raae]), and is 0 when snap_id is 0x0000. Under snap_id 0x0003 the header also carries an epoch-heads region of E * Nh octets, where E = ceil(N / 2^r) is the epoch count at the epoch length r that the selected instantiation resolves to (Section 5.8.1). A suite that takes a flat key gives E = 1 and a single-epoch fold, which is correct and simply carries no random-access benefit (Section 5.9.6 of [I-D.sullivan-cfrg-raae]).¶
Each row below represents one Segment-Size:¶
Each metadata entry is the raAE metadata entry of Section 4.11.2 (Aligned Layout) of [I-D.sullivan-cfrg-raae].¶
The binary portion immediately follows the text headers. The first two fields are SAFE's, and the rest are the raAE header in that document's order:¶
N: segment count (uint32, big-endian)¶
D: first ciphertext segment index (uint32, big-endian)¶
salt (32 octets)¶
commitment (Nh octets)¶
snapshot value: Na octets, absent when snap_id is 0x0000 (Section 5.8.7)¶
epoch heads: E * Nh octets, present only when snap_id is 0x0003¶
metadata: N entries, each meta_len octets¶
padding to segment boundary, then zero or more padding segments¶
ciphertext segments starting at offset (D × B)¶
The uint32 segment count limits aligned-layout files to 2^32 - 1 segments. The Profile fixes the layout (Section 5.8.1), so an object needing more segments is not representable under an aligned-layout Profile: an Encryptor selects a linear-layout Profile instead.¶
To write an aligned-layout file, the Encryptor computes D as follows. Let H be the total octet count of everything before the ciphertext: all CONFIG and LOCK text (including fence markers and newlines), plus 4 (N) + 4 (D) + 32 (salt) + Nh (commitment, 64 for TurboSHAKE-256) + Na (the snapshot value) + E * Nh (the epoch heads, only when snap_id is 0x0003) + N * meta_len (metadata). Then:¶
D = ceil(H / B)¶
Padding segments for append growth MAY be added by increasing D beyond the minimum value. The encryptor writes the header, pads to D * B octets, then writes ciphertext segments at offsets D * B, (D+1) * B, and so on.¶
The aligned layout requires a seekable output. For streaming writes where N is unknown at the start, the Encryptor estimates a maximum segment count, computes D from that estimate, reserves space for N, D, the metadata, and the epoch heads when snap_id is 0x0003, then seeks back to fill the actual values once the stream closes. E follows from N, so an estimate that bounds N bounds the heads region too.¶
To read an aligned-layout file:¶
Parse CONFIG and LOCK text to determine the Profile and AEAD.¶
Read N and D, then the 32-octet salt and the Nh-octet commitment.¶
Re-derive the commitment and reject the object on mismatch (Section 8.13). The commitment is reachable from the salt and the text headers alone, so this precedes any allocation sized by N, which nothing has authenticated yet.¶
When snap_id is not 0x0000, read the Na-octet snapshot value.¶
When snap_id is 0x0003, read the epoch-heads region, E * Nh octets.¶
Read N metadata entries, each meta_len octets. Under snap_id 0x0002 and 0x0003 an entry stores that segment's leaf.¶
Verify the snapshot before relying on anything it covers (Section 5.8.7), using a constant-time comparison and rejecting the object on mismatch. Under snap_id 0x0001 this is SnapVerify over the whole segment set. Under 0x0002 and 0x0003 the authenticator verifies from the header alone, so a Decryptor reading one segment follows the per-segment procedure of Section 4.7.5 or 4.7.6 of [I-D.sullivan-cfrg-raae], which consumes the regions read in steps 4 through 6.¶
Ciphertext segment i is at offset (D + i) × B.¶
To read only segment i, the nonce source follows the resolved nonce mode. In derived mode (nonce_mode 0x01) compute nonce_i from nonce_base and the segment index. In random mode (nonce_mode 0x00) read the nonce from the metadata entry. Read the tag from the metadata entry in both cases. Then read B octets (or fewer for the final segment) at offset (D + i) x B. The final segment's ciphertext length is file_size - (D + N - 1) * B octets.¶
Decryptors processing SAFE-encoded data MUST:¶
Fail if they encounter an unrecognized or unimplemented value for any CONFIG field (Profile, AEAD, KDF, Data-Encoding, Lock-Encoding). Implementations MUST NOT silently ignore CONFIG values they do not support.¶
Reject a Profile value that Section 5.8.1 does not define.¶
Skip LOCKs containing unknown field names, KEM identifiers, or step types and attempt other LOCKs (if available).¶
Fail if a CONFIG block contains field names not listed in the SAFE Config Options registry (Section 10.5).¶
Fail if the payload has unexpected structure (wrong commitment length, trailing octets, misaligned segment boundaries).¶
Skip unknown block types if the IANA SAFE Block Types registry (Section 10.6) marks them as Ignorable; otherwise fail.¶
This document defines SAFE version 1, identified by fence markers ("-----BEGIN SAFE CONFIG-----", etc.). Future incompatible versions would use different fence markers or a new media type. New features SHOULD be added through IANA registries rather than format version changes.¶
SAFE provides IANA registries for AEADs (Section 10.1), KEMs (Section 10.2), step types (Section 10.4), and block types (Section 10.6).¶
Unknown block types are critical by default: Decryptors MUST fail if they encounter an unrecognized block. The IANA SAFE Block Types registry (Section 10.6) MAY mark specific block types as Ignorable, enabling forward-compatible optional extensions such as metadata or signatures that older implementations can safely skip.¶
This section is informative. It describes parameter combinations for common deployment scenarios. These profiles compose the CONFIG fields defined in Section 6.1 and do not introduce new protocol elements.¶
Applications that prioritize text-safe output and maximum interoperability SHOULD use the default parameters (Section 5.1) with armored data:¶
-----BEGIN SAFE CONFIG----- Data-Encoding: armored -----END SAFE CONFIG-----¶
The resulting SAFE object is entirely printable ASCII and can be embedded in email, JSON, YAML, or version-controlled files. AES-256-GCM is the default AEAD (Section 5.1).¶
Applications that process data sequentially at high throughput SHOULD consider:¶
-----BEGIN SAFE CONFIG----- AEAD: aegis-256 -----END SAFE CONFIG-----¶
AEGIS-256 offers high throughput and a 32-octet nonce that simplifies nonce management. The default Profile SEAL-editable uses a linear layout, so binary encoding (Section 6.4.1) yields minimal framing overhead and sequential I/O without alignment padding. Encryptors using this profile SHOULD apply the hedged nonce construction (Section 5.4.2.2) or plaintext-bound nonce construction (Section 5.8.5.1) per Section 8.10.¶
Encryptors targeting broad interoperability SHOULD verify recipient support before selecting this profile.¶
Applications that perform random-access reads and writes on encrypted data SHOULD consider:¶
-----BEGIN SAFE CONFIG----- Profile: SEAL-compact AEAD: aes-256-gcm-siv -----END SAFE CONFIG-----¶
The SEAL-compact profile is derived-nonce and aligned (Section 5.8.1). Per-segment nonces are derived rather than stored (Section 5.8.4), reducing per-segment metadata from Nn + 16 octets to 16 octets. The SEAL-compact profile fixes Segment-Size 16384, aligning each segment to a single page on 16 KiB-page systems and minimizing page faults per edit. Binary aligned encoding (Section 6.4.2) enables O(1) random access to any segment via memory-mapped I/O.¶
Re-encrypting a modified segment reuses the derived nonce for that segment index. Because AES-256-GCM-SIV is misuse-resistant, this degrades to deterministic encryption for unchanged segments rather than catastrophic nonce reuse (Section 8.10.2).¶
Applications that require FIPS 140-validated algorithms for random-access editing SHOULD consider:¶
-----BEGIN SAFE CONFIG----- Profile: SEAL-memory AEAD: aes-256-gcm -----END SAFE CONFIG-----¶
AES-256-GCM is FIPS 140-validated; ChaCha20-Poly1305, AES-256-GCM-SIV, AEGIS-256, and AEGIS-256X2 are not currently covered by a FIPS 140 validation program. The SEAL-memory profile fixes Segment-Size 16384 and takes its instantiation's epoch length (Section 5.8.1), deriving segment keys through the epoch schedule (Section 8.14.2).¶
Unlike SEAL-compact (Section 7.4.3), this profile stores random per-segment nonces (Nn + 16 octets per segment vs. 16 octets in derived mode). Applications that can tolerate segment-level equality leakage on rewrite SHOULD prefer SEAL-compact for its lower per-segment overhead.¶
SAFE provides:¶
ra-ROR (random-access real-or-random) security for the payload, the joint confidentiality and authenticity notion for segmented random-access encryption formalized in [FLOE]. An adversary with adaptive encryption and decryption access cannot distinguish ciphertext segments from random and cannot make an out-of-context segment decrypt. SafeDerive is a PRF, so payload_key is indistinguishable from random, and under a random key the SEAL payload layer provides ra-ROR per the analysis in [FLOE]. The commitment prefix meets the companion ra-CMT (random-access context commitment) notion (Section 8.13). ra-ROR gives per-segment integrity only. Whole-object integrity comes from the snapshot authenticator when snap_id is not 0x0000, as described under Authentication.¶
Each LOCK's Encrypted-CEK is authenticated via AEAD under derived_kek, which binds step bindings and step secrets through the KEK schedule. Segment AEAD binds each segment to its index (Section 8.3), preventing reordering, modification, and splicing. The snapshot authenticator (Section 5.8.7), present in every Profile except SEAL-simple (Section 5.8.1), provides file-level integrity and authenticity over every segment's per-segment input and the segment count.¶
The KEK schedule binds encryption_parameters in the LOCK's single KEK derivation (Section 5.7.1). All step bindings and per-step secrets are bound in that one SafeDerive call. Payload keys inherit suite binding from their own SafeDerive calls (Section 5.8.2).¶
SAFE does not provide:¶
Without a sender parameter (sid or shint), any party with recipient public keys can create SAFE-encoded data. See Section 8.2 for Auth mode authentication properties.¶
CEK compromise exposes all recipients' copies. This is inherent to stored-object encryption, which has no interactive key exchange.¶
Key identifiers enable linking SAFE-encoded data to the same recipient. See Section 8.7 for privacy modes.¶
SAFE assumes secure key storage, side-channel resistant implementations, and trusted cryptographic primitives. A functioning CSPRNG is REQUIRED. See Section 8.10 for defenses against RNG weakness or state duplication.¶
SAFE defends against:¶
An adversary with read access to stored SAFE-encoded data cannot decrypt without valid credentials (passphrase or private key) for at least one LOCK. The adversary can observe approximate file size, recipient count, and key identifier linkability. SAFE detects segment corruption (AEAD failure), truncation (is_final mismatch), and segment-count truncation or extension (the snapshot authenticator binds the segment count), but does not detect LOCK removal, LOCK addition, or whole-file replacement under a different CEK by an adversary with write access.¶
SAFE does not defend against:¶
If a recipient's credentials (passphrase or private key) are compromised, the adversary can decrypt the payload. All recipients share the same CEK; compromise of one recipient's KEK does not expose other recipients' KEKs, but does expose the shared CEK and payload. The weakest LOCK determines the effective security of the entire file: an attacker who can satisfy any single LOCK recovers the CEK.¶
If an attacker compromises a recipient's private key and can modify files, they can create valid SAFE-encoded data for that recipient (in Base mode). Auth mode (Section 5.6.3.1) mitigates this for steps where the attacker does not also hold the sender's private key; see Section 8.2.¶
SAFE assumes implementations do not intentionally leak secrets. Timing attacks on Argon2id, HPKE, or AEAD operations are out of scope for this document.¶
Any party with a recipient's public key can create valid SAFE-encoded data for that recipient. SAFE does not constrain what an Encryptor can encrypt or for whom. Applications MUST validate decrypted content independently of the encryption envelope.¶
When sid or shint is present in an hpke(...) step, SAFE uses HPKE
Auth mode (mode_auth, [RFC9180]). Auth mode defends against forgery by
parties who do not hold the sender's private key: a decryptor who
successfully processes an auth-mode step is assured that the
encapsulation was produced by a holder of skS. This closes the
"encryptor authentication" gap identified above for Base mode, within
the following limits:¶
Auth mode authenticates the sender only to the holder of the recipient's private key. The recipient cannot prove to a third party that the sender created the SAFE object. Applications requiring non-repudiation MUST use external signatures.¶
The sid parameter (when a Base64 value) reveals the sender's key
identifier to any observer. shint narrows the sender's identity.
Anonymous Auth mode (sid=anon) avoids explicit sender
identification, at the cost of trial decryption across all candidate
sender keys.¶
SAFE does not define a trust model for sender public keys. Decryptors MUST independently verify that a sender's public key is authentic (e.g., via a certificate, trust on first use (TOFU), or out-of-band verification) before relying on auth-mode authentication.¶
The KEK schedule binds encryption_parameters and all step bindings and per-step secrets in a single SafeDerive call. The payload schedule binds payload_key to payload_info independently, tying segment encryption to the negotiated AEAD, Segment-Size, and the other SEAL parameters. Payload AEAD binds each segment to its index and its finality, preventing reordering and cross-file splicing, and SAFE detects truncation and extension at segment boundaries from that finality binding. Whether it lives in the segment AAD or in the nonce follows the resolved nonce mode (Section 5.8.8).¶
The snapshot authenticator (Section 5.8.7) provides file-level integrity for every Profile whose snap_id is not 0x0000 (Section 5.8.1). Its detection set, construction, and security are specified there and in Section 4.7 of [I-D.sullivan-cfrg-raae]. The is_final flag bound to each segment additionally detects a file that ends without a final-marked segment (Section 5.8.8).¶
The snapshot authenticator provides no freshness. A complete earlier snapshot of the file, presented with its matching snapshot value, verifies as valid (Section 6.6 of [I-D.sullivan-cfrg-raae], Snapshot Integrity Limitations). Applications that require rollback protection MUST bind the current snapshot value to external trusted state.¶
Applications requiring third-party verifiability (e.g., signatures) MUST use external signatures.¶
SAFE's payload layer derives in the selected SEAL domain (SEAL-RW-v1 or SEAL-RO-v1, Section 5.8.1), shared with any application that adopts the same SEAL profile verbatim.¶
Identical derivations across applications require both the same CEK and the same payload_info (Section 6.11 of [I-D.sullivan-cfrg-raae], Cross-Application Domain Separation). The fresh 32-octet random salt SAFE generates per file is the final payload_info element, so the condition requires reusing both a CEK and a salt across applications. An application pair that reuses both accepts the other's payload-layer objects as valid.¶
The SAFE key layer (KEK schedule, step secrets, and key identifiers) remains in the SAFE-v1 domain.¶
The step sequence has AND semantics: an attacker must break every step to recover the CEK, so security is at least that of the strongest step. Because the KEK is a single combiner call over all step secrets, an attacker who learns a proper subset of them learns nothing about the KEK.¶
lock_nonce MUST be fresh per LOCK (Section 5.7.2). Per-segment nonces follow the resolved nonce mode (Section 5.8.4).¶
Implementations MUST zeroize sensitive values (CEK, KEK, PRKs, payload_key, snap_key, nonce_base) immediately after use. Long-lived processes that retain a CEK (e.g., for incremental writes) SHOULD store it in swap-protected memory (e.g., mlock).¶
To prevent error oracles, implementations exposing decryption to untrusted callers (e.g., network services, APIs) MUST return a single generic "decryption failed" error rather than distinguishing between wrong passphrase, wrong key, commitment mismatch, or AEAD failure. Local tools (e.g., CLI applications, test harnesses) MAY use the detailed error codes in Appendix C for diagnostics.¶
Implementations MUST evaluate the cryptographic operations that touch secret data in constant time, as required by Section 6.9 (Constant-Time Implementation) of [I-D.sullivan-cfrg-raae]. The SAFE-specific surfaces this covers are passphrase KDF evaluation and Base64 decoding of secret material (Encrypted-CEK, step parameters carrying key material). Commitment and snapshot-value comparisons MUST use a constant-time equality function (e.g., CRYPTO_memcmp or equivalent), and trial decryption loops (Section 8.7.3) MUST NOT leak timing information about which candidate key succeeded.¶
SAFE supports two passphrase KDF variants with different security properties:¶
Memory-hard function that resists GPU and ASIC attacks. The default parameters (64 MiB memory, 2 iterations) provide strong resistance to offline attacks. Recommended for most deployments.¶
Widely deployed function using PBKDF2-HMAC-SHA-256. Lacks memory-hardness, making it more vulnerable to GPU and ASIC attacks than Argon2id. The 600,000 iteration count provides equivalent CPU-based attack resistance but does not mitigate hardware-based attacks. Use only when policy prohibits memory-hard KDFs.¶
Encryptors targeting Decryptors with mixed policy constraints MAY include two pass(...) LOCK blocks: one with pass(kdf=argon2id, ...) and one with pass(kdf=pbkdf2, ...), using the same passphrase but fresh salts for each.¶
A captured SAFE object gives an attacker an offline guessing oracle against passphrase-only LOCK blocks: each candidate passphrase is tested by deriving the KEK and attempting to open the LOCK's Encrypted-CEK (AEAD tag verification, Section 5.7.2), with no online interaction and no rate limit. KDF stretching raises the per-guess cost but does not remove the oracle, so the passphrase's entropy, not the KDF, is the decisive defense. Applications SHOULD therefore enforce a minimum passphrase complexity policy (e.g., at least 20 characters or equivalent entropy). For high-value data, Encryptors SHOULD combine a pass(...) step with an hpke(...) step in the same LOCK, so that compromise of the passphrase alone is insufficient.¶
SAFE supports three levels of recipient identification for hpke(...) steps:¶
The id parameter uniquely identifies the recipient's public key.
Observers can link SAFE-encoded data encrypted to the same
recipient.¶
The hint parameter is a recipient-assigned value (not
cryptographically derived). It filters candidates locally while
revealing nothing about the key itself. Multiple keys may share the
same hint.¶
No identifier is present. Decryptors MUST trial-decrypt against all
local keys matching the kem type. Provides maximum privacy at the
cost of increased decryptor computation.¶
These modes are properties of the LOCK, not of its encoding. The
readable and armored forms (Section 6.2.2) carry the same step
descriptors, so an id or hint is present in both encodings or in
neither, and anonymous mode omits both in both encodings. A passive
observer therefore learns a linkable key identifier only when the
encryptor chooses identified mode, whichever Lock-Encoding is used.¶
Omitting or replacing the key identifier with a hint prevents passive observers from mapping SAFE-encoded data to specific public keys. This is valuable when file-recipient associations are sensitive metadata.¶
Auth-mode hpke(...) steps support the same three levels of sender
identification via sid and shint:¶
sid present):The sid parameter identifies the sender's public key using the
same KDF as id. Observers can link SAFE objects to the same
sender across files.¶
shint present):The shint parameter is a sender-assigned value that filters
candidates locally. It reveals less than sid but still narrows
the sender's identity.¶
sid=anon):Decryptors trial-decrypt against all locally known sender keys
matching the kem type. Provides sender privacy at the cost of
increased trial decryption (see Section 8.7.3).¶
Encryptors SHOULD prefer sid unless sender privacy is required. The
same trade-offs between identification, hinting, and anonymity apply to
sender keys as to recipient keys.¶
Anonymous mode with composable step sequences (multiple hpke steps)
requires combinatorial trial decryption. For a step sequence with two
anonymous hpke(...) steps, where the Decryptor holds K1 keys for step 1
and K2 keys for step 2, up to K1 x K2 combinations may be attempted.
Auth-mode steps add a further multiplicative factor: if an auth-mode
step uses sid=anon, the Decryptor MUST try all S candidate sender
keys, multiplying the search space by S.¶
Implementations MUST set a MaxTrialAttempts limit to bound computation and MUST reject LOCK blocks that would exceed this limit. A value of 1024 is RECOMMENDED; implementations MAY adjust based on deployment constraints.¶
An attacker can craft SAFE-encoded data with many anonymous LOCK blocks to force Decryptors into expensive cryptographic operations. Implementations MUST:¶
Limit the number of LOCK blocks processed per object¶
Prioritize identified blocks over hinted blocks over anonymous blocks¶
Abort early when resource limits are exceeded¶
Implementations MUST also limit the number of steps per LOCK block. A limit of 16 steps per LOCK is RECOMMENDED; this prevents an attacker from crafting a single LOCK block that forces evaluation of an excessive number of passphrase KDF computations. Implementations SHOULD limit the total number of passphrase KDF evaluations to 8 per file; an attacker who crafts multiple LOCKs with pass(...) steps can otherwise force expensive Argon2id computations proportional to the LOCK count.¶
ML-KEM decapsulation is significantly more expensive than X25519; anonymous ML-KEM steps amplify the DoS potential. See Section 6.2.3 for the recommended per-credential LOCK bound.¶
The hint is a 4-digit decimal value (0000-9999) assigned by the recipient; it is not solely dependent on the public key. Recipients communicate their hint to Encryptors out-of-band. Multiple keys MAY share the same hint.¶
Encryptors MUST NOT assume the hint uniquely identifies a key. Decryptors MAY reassign hints at any time; Encryptors SHOULD refresh hint values periodically through out-of-band communication.¶
Encryptors SHOULD prefer identified mode unless recipient privacy is required.¶
Encryptors SHOULD use the hedged construction (Section 5.4.2.2) when a private key is available, the plaintext-bound nonce construction (Section 5.8.5.1) when RNG state duplication is a concern, and an MRAE AEAD (Section 5.2.1) for additional protection. The following cases describe the resulting security properties.¶
Full protection. The segment nonce base is derived from both fresh randomness and the hedge key. Nonces are unique across files and within files.¶
Deterministic encryption per Encryptor. Different Encryptors (with different private keys) produce different hedge keys and therefore different CEKs and nonce bases. Within a single file, segment indices guarantee nonce uniqueness. Across files from the same encryptor, the CEK and salt repeat, producing identical payload keys and ciphertext for identical plaintext segments at the same index. This leaks equality but not content. The plaintext-bound nonce construction (Section 5.8.5.1) further limits exposure: nonces differ when plaintext differs, even across files.¶
Full protection. SafeRandom returns raw CSPRNG output. Nonce uniqueness depends on the RNG.¶
No defense. CEKs and salts repeat across files, producing identical payload keys and nonce bases. Within a file, segment indices still provide distinct nonces. Across files, nonce reuse under distinct plaintext permits key recovery attacks against AES-GCM, ChaCha20-Poly1305, AEGIS-256, and AEGIS-256X2. AES-256-GCM-SIV limits the damage to deterministic encryption (leaks equality). The plaintext-bound nonce construction also limits nonce reuse to identical segments at the same index.¶
A functioning CSPRNG is REQUIRED when no private key is available.¶
RNG state duplication arises from VM snapshots, process forks without reseed, and container cloning. Because the plaintext-bound construction incorporates a collision-resistant digest of the segment plaintext into nonce derivation (Appendix A of [I-D.sullivan-cfrg-raae]), two instances that share identical key material still produce distinct nonces whenever plaintext differs, which justifies its two-pass cost.¶
Within a single file, segment indices are bounded by the plaintext length and Segment-Size. Encryptors MUST ensure segment indices remain below 2^64 in random mode and below 2^63 in derived mode (Section 5.8.8). Practical implementations SHOULD enforce a lower bound; for example, rejecting plaintexts exceeding 2^48 segments (approximately 16 EiB at the default Segment-Size of 65536 octets) provides a conservative margin while supporting files far larger than current storage systems.¶
Only a mutable Profile can be extended (Section 5.8.1). Appending data to such a file requires re-encrypting the old final segment (which had is_final=1) with is_final=0, then encrypting the new segments. Encryptors MUST NOT generate a new CEK or salt. The existing LOCKs and salt are reused. Encryptors MUST run SnapVerify before extending the file. Extending a corrupted file propagates undetected damage. The procedure is:¶
Decrypt the current final segment (index N-1) and verify is_final=1.¶
Re-encrypt segment N-1 with is_final=0. In random mode (nonce_mode 0x00), Encryptors MUST generate a fresh nonce using SafeRandom (Section 5.4.2). In derived mode (nonce_mode 0x01), the nonce binds is_final (Section 5.8.4), so re-encrypting segment N-1 with is_final changed from 1 to 0 yields a different nonce, not a repeated one. See Section 8.10.2.¶
Encrypt new segments N through N+K-1 with is_final=0.¶
Encrypt segment N+K (the new final segment) with is_final=1.¶
Update the metadata entries for the changed and new segments, and, under an aligned-layout Profile, the stored segment count N. A linear layout stores no count (Section 6.4.1).¶
Update the snapshot authenticator for the changed segments and re-seal the snapshot value (Section 5.8.7).¶
The ciphertext of segment N-1 changes, so an observer can detect that it was re-encrypted, but its content is not revealed.¶
In derived mode (nonce_mode 0x01), per-segment nonces are derived deterministically from nonce_base and the segment index rather than generated randomly and stored. The conditions under which this is safe (an MRAE AEAD for the rewrite reuse the mutable profile permits, the write-once discipline for a non-MRAE AEAD) are given in Section 4.5.3.2 of [I-D.sullivan-cfrg-raae].¶
One uniqueness anchor is SAFE-specific. When the immutable (write-once) profile is used with a non-MRAE AEAD and a CEK is reused across objects, the per-file salt is the sole anchor of cross-object nonce uniqueness: nonce_base = SealDerive("nonce_base", CEK, payload_info, Nn), and payload_info includes the per-file salt (Section 5.8.2). A duplicated salt therefore repeats every derived nonce across the two objects, a plaintext-recovery failure. raAE mandates a fresh salt per write-once object (Section 4.5.3.2 of [I-D.sullivan-cfrg-raae]).¶
Per-segment nonces and the is_final flag enable selective editing: individual segments can be re-encrypted without affecting other segments or LOCK blocks. When editing:¶
Source the re-encrypted segment's nonce per the resolved nonce mode: a fresh random nonce in random mode, or the segment's derived nonce in derived mode, whose reuse the required MRAE AEAD tolerates (Section 5.8.6, Section 8.10.2)¶
Update the is_final flag if the last segment changes¶
Update the snapshot authenticator and re-seal the snapshot value (Section 5.8.7)¶
Segments not being edited retain their original nonces and ciphertexts¶
The is_final flag prevents truncation and extension attacks:¶
Truncation: decrypting a segment with is_final=0 when no successor exists indicates malicious or accidental truncation¶
Extension: appending segments after a segment with is_final=1 will fail AEAD verification, because the original final segment is bound to is_final=1 (Section 5.8.8)¶
Key identifiers are 32-octet hashes of SPKI DER encodings (Section 5.6.3.3). The id derivation (Section 5.6.3.3) requests 32 octets from either registered KDF (hkdf-sha-256 or turboshake256) by passing L=32 to SafeDerive, giving a birthday bound on collision probability of approximately N^2 / 2^257 for a deployment with N keys. This is negligible for any practical key population.¶
For an SPKI that exceeds the 0xFFFE-octet literal limit, the key identifier composes two 256-bit hash steps (the over-large-field digest LH of Section 4.3 (The KDF Combiner) of [I-D.sullivan-cfrg-raae] over the SPKI, then SafeDerive over the resulting digest entry). The collision bound is unchanged because a collision still requires a 256-bit hash collision.¶
Implementers MUST NOT rely solely on key identifier matching for authorization; successful HPKE decapsulation and AEAD verification of Encrypted-CEK are required.¶
The commitment is the SEAL commitment (Section 4.6 of [I-D.sullivan-cfrg-raae], Commitment), derived as Section 5.8.2 gives.¶
SAFE supports multiple LOCK blocks that can be added or removed independently without re-encrypting the payload, because each LOCK wraps the same CEK. However, removing a LOCK does not revoke access: any party who previously decrypted the CEK retains the ability to decrypt the payload. Applications requiring revocation MUST generate a new CEK and re-encrypt.¶
SAFE's external Nh-octet commitment prefix (Section 5.8.2) is the sole check that binds the Decryptor's resolved protocol_id and nonce mode to the file. A Decryptor MUST verify it before any segment key derivation (Section 5.8.2). Why this prefix is needed, why no registered AEAD supplies equivalent commitment from its own mechanism, and the random-access context-commitment (ra-CMT) target it meets are analysed in Section 5.7 (Commitment Security) of [I-D.sullivan-cfrg-raae].¶
The check binds the ciphertext to the CEK, the negotiated algorithm parameters, and the per-file salt, preventing cross-algorithm commitment collisions. The derivation is given in Section 5.8.2, and the collision bound (which rests on the collision resistance of the commit derivation, not on PRF security) in Section 5.7 (Commitment Security) of [I-D.sullivan-cfrg-raae].¶
The commitment prefix also covers the KEK layer. None of the registered AEADs used for CEK wrapping (Section 5.7.2) is key-committing, and nothing forces distinct LOCK blocks to wrap the same CEK, so a malicious encryptor can construct LOCKs that unwrap to different CEK values for different recipients, the failure class [AGE-COMMIT] demonstrates against age. The commitment check closes this gap. Each Decryptor re-derives the commitment from its recovered CEK and rejects on mismatch before any segment key derivation, and producing two CEK values that both match the stored prefix requires a collision in the commit derivation. Recipients that accept the same object therefore hold the same CEK and decrypt identical plaintext.¶
Per-AEAD confidentiality and integrity bounds are analysed in Section 5.9 of [I-D.sullivan-cfrg-raae], Capacity and Usage Limits. This section adds the SAFE-specific considerations that arise from multi-recipient sharing of a single CEK, lifetime rewrite accounting, and the relationship to SAFE's uniform key commitment.¶
See Section 8.10 for defenses against RNG weakness and state duplication, including the hedged nonce construction (Section 5.4.2.2, [RFC8937]). Key commitment (Section 8.13) and segment-level integrity (Section 5.8.8) address threats orthogonal to nonce discipline and are analysed in Section 5.7 (Commitment Security) and Section 5.6 (Segment Confidentiality and Integrity) of [I-D.sullivan-cfrg-raae].¶
Let q denote the total number of segment encryptions under one payload key, counting every initial segment write and every segment rewrite. Because all recipients of a SAFE object share one CEK and therefore one payload key, the per-key bound from [I-D.sullivan-cfrg-raae] applies regardless of recipient count; adding recipients does not increase q.¶
The same q can be reached by a large initial write or by repeated rewrites of a smaller file. A 1 GiB file contains 2^14 segments at the default segment size; rewriting the entire file 1024 times produces approximately 2^24 total segment encryptions, the same q as encrypting 1 TiB once. The relevant quantity is lifetime encrypted segments per payload key, not current file size.¶
In a multi-user setting where an attacker targets any one of U independently keyed SAFE files, the effective nonce-collision probability for 96-bit-nonce AEADs is approximately:¶
U * q^2 / 2^(8*Nn + 1)¶
For U = 2^20 files each at q = 2^32, this is approximately 2^(-13), which is not negligible. The epoch key schedule (Section 8.14.2) mitigates this by spreading segment encryptions across multiple epoch keys instead of one payload key: the 96-bit-nonce suites use epoch length 16 for the mutable instantiations (SEAL-editable, SEAL-memory, SEAL-compact), so one epoch key covers 2^16 segments rather than the whole file. This document does not restate a mitigated multi-user bound for epoch length 16: the per-key q at this granularity depends on how rewrites distribute across the 2^16 segments sharing an epoch key, which needs its own derivation and is not a simple substitution into the single-segment (epoch length 0) case. The 256-bit nonce AEADs (AEGIS-256, AEGIS-256X2) take a flat key regardless of Profile (Section 5.8.1), and their nonce size renders multi-user bounds operationally irrelevant in any case (approximately 2^(-173) at the same parameters).¶
Each epoch key, not the CEK, carries the AEAD encryption budget (Section 5.9.1 of [I-D.sullivan-cfrg-raae]). Applications MUST track segment encryptions per key and freeze the object before a budget is exceeded (Section 5.9.5 of [I-D.sullivan-cfrg-raae]); an implementation that reaches a budget re-encrypts the object under a fresh CEK rather than continuing under the same key. At the epoch length SAFE pins for the mutable instantiations (Section 5.8.1), the per-segment rewrite share is roughly 2^(32 - epoch_length) for the 96-bit-nonce suites, so repeated rewrites of one segment are the case that reaches it first (Section 6.8 of [I-D.sullivan-cfrg-raae]).¶
The epoch key schedule (Section 5.8.3) limits AEAD invocations per epoch key, so the birthday bound applies per epoch key rather than file-wide. The instantiation's pin (Section 5.8.1) trades some per-key rotation for fewer epoch-key derivations across rewrites, and a caller cannot coarsen it further to trade rewrite budget for fewer key derivations. Epoch keys are derived, not stored, so no LOCK re-wrapping is needed. The per-epoch rewrite budget and the hot-segment concentration case are in Section 5.9.5 (Rewrite Capacity) and Section 5.9.6 (Choosing epoch_length) of [I-D.sullivan-cfrg-raae].¶
The choice of layout (linear or aligned, Section 6.4) does not change the cryptographic analysis; it affects only framing and on-disk storage.¶
SAFE accommodates post-quantum KEMs without format changes. ML-KEM-768 (HPKE KEM ID 0x0041) is registered and MAY be used as kem=ml-kem-768.¶
Hybrid post-quantum constructions require no protocol extensions. An encryptor lists both a classical and a post-quantum hpke(...) step in the same step sequence:¶
Step: hpke(kem=x25519, kemct=<Base64>, id=<classical-id>) Step: hpke(kem=ml-kem-768, kemct=<Base64>, id=<pq-id>)¶
The KEK schedule (Section 5.7.1) binds both steps' secrets in one derivation, so the derived KEK depends on both the X25519 and ML-KEM-768 shared secrets, and an attacker must break both KEMs to recover it. Because every step secret enters that single derivation, the LOCK's security is conjunctive in its steps. Hybrid post-quantum protection follows naturally from combining classical and post-quantum steps.¶
The decryptor evaluates both decapsulations during CEK recovery. Each
KEM ciphertext is carried in the kemct parameter of its corresponding
hpke(...) step descriptor. See Appendix A for a complete example.¶
Implementations planning PQ migration SHOULD ensure kemct parsing does
not impose unnecessary length limits (ML-KEM-768 ciphertexts are 1088
octets).¶
Auth mode (Section 5.6.3.1) relies on DHKEM AuthEncap/AuthDecap, which requires a CDH-hard group. No post-quantum KEM currently supports Auth mode; ML-KEM-768 MUST NOT be used with Auth mode (Section 5.6.3.2). Applications requiring post-quantum sender authentication MUST use external signatures.¶
SAFE derives all symmetric keys at 256 bits. The AEAD tag length (128 bits) and the commitment length (Nh octets, 256 bits for HKDF-SHA-256 and 512 for TurboSHAKE-256) provide at least 128-bit security against forgery and collision attacks. The snapshot authenticator provides file-level integrity at the same level (Section 5.8.7).¶
The Encode function is injective, so step_bindings are unambiguous; injectivity extends to over-large fields via the reserved 0xFFFF digest prefix, as established in Section 5.5 (raAE Construction Requirements) and Section 4.3 (The KDF Combiner) of [I-D.sullivan-cfrg-raae].¶
The reserved value has no effect on any current encoding. Every field that SAFE encodes today is below 0xFFFF octets, so all existing encodings, step_bindings, keys, commitments, and the test vectors in Appendix G are byte-identical to those produced before 0xFFFF was reserved. Only a field exceeding 0xFFFE octets takes the digest path; no such field exists in the current registrations. No current test vector changes.¶
Per-segment nonces are unique within a file by construction. In random mode (nonce_mode 0x00) each nonce is generated via SafeRandom. In derived mode (nonce_mode 0x01) each segment's derived nonce is distinct because it is bound to the segment index (Section 5.8.4). The relevant birthday bound for nonce collisions is across files or across segment rewrites under the same payload key. See Section 8.14 for per-AEAD analysis.¶
SAFE has no algorithm negotiation: the Encryptor selects encryption_parameters unilaterally, and the Decryptor either accepts them or fails. This downgrade-resistance property covers only the suite options bound into encryption_parameters (Section 5.5): aead_id, segment_size_be, kdf_id, snap_id, and epoch_length_u8. An active attacker who modifies any of these (e.g., substituting a weaker AEAD) changes the derived KEK (Section 5.7.1) and payload keys (Section 5.8.2), causing CEK unwrapping or segment decryption to fail. Forging a valid SAFE object with altered suite options requires the attacker to also hold valid credentials for the target LOCK.¶
Representation options (Lock-Encoding and Data-Encoding) fall outside encryption_parameters and outside this binding. An attacker who transcodes an object between armored and binary form, or between readable and armored LOCK encoding, changes no cryptographic value, since the encodings are lossless transcodings of one another (Section 3). Tampering with representation options alone is unauthenticated but safe: it cannot force acceptance of a weaker suite, and any change to the underlying bytes beyond re-encoding is still caught by the authentication that covers the suite options.¶
Decryptors SHOULD enforce a locally configured allowlist of acceptable encryption parameters. Rejecting algorithms outside the allowlist limits the attack surface to supported primitives.¶
The symmetric components of SAFE (HPKE export-only key schedules, SafeDerive-based KEK derivation, commitment prefixes, and AEAD encryption) are not vulnerable to quantum attacks. Grover's algorithm provides at most a quadratic speedup against symmetric primitives, leaving all symmetric operations at or above 128-bit security. The post-quantum migration surface is limited to KEM selection.¶
A deployment using KDF=turboshake256 eliminates all SHA-256 dependencies: per [I-D.ietf-hpke-hpke], all KEMs use that KDF internally.¶
This section addresses the privacy properties of SAFE per [RFC6973]. SAFE is primarily a data-at-rest format; it does not define a transport protocol, so many [RFC6973] considerations (correlation by IP, traffic analysis of flows) do not apply directly.¶
SAFE-encoded data reveals the following metadata to passive observers:
approximate payload size (from Base64 or binary length), recipient count
(from the number of LOCK blocks), and, when key identifiers are present,
linkability across files encrypted to the same recipient
(Section 8.7). Hinted mode (Section 8.7.1) reduces
linkability; anonymous mode eliminates key-identifier-based linking at
the cost of increased trial decryption. Auth-mode sender identifiers
(sid, shint) create analogous sender linkability
(Section 8.7.2). Applications concerned about metadata leakage
SHOULD pad payloads and SHOULD prefer hinted or anonymous modes.¶
Beyond the metadata listed above, the step types and KEM identifiers in each LOCK are visible in cleartext, revealing the authentication factors required (e.g., passphrase, X25519, ML-KEM-768). In multi-recipient files, the set of LOCK blocks reveals co-recipient relationships. In derived mode, segment rewrites at the same index produce identical ciphertext when the plaintext is unchanged, leaking equality (Section 8.10.2). A comprehensive [RFC6973] privacy analysis is deferred to a future revision.¶
IANA is requested to create a SAFE AEAD Identifiers registry. Registration policy is Specification Required. Designated Experts should verify that proposed AEADs provide [RFC5116] semantics with a 16-octet authentication tag and Nk of 32 octets (SAFE derives all keys at 256 bits). Experts MUST reject registrations where Nk is not 32. Identifiers MUST consist of lowercase ASCII letters, digits, and hyphens, and MUST NOT exceed 255 octets. Identifier comparison is byte-exact; see Section 5.5. Each entry MUST include a Binding Name: an informative registry name, taken from the IANA AEAD Algorithms registry [RFC5116] when the algorithm is registered there. The value bound into encryption_parameters and payload_info is the numeric AEAD ID, not the Binding Name. The algorithm MUST be appropriate for general-purpose use in encrypted data formats.¶
Initial entries:¶
| Identifier | Binding Name | AEAD ID | Nk | Nn | MRAE | Reference | Change Controller |
|---|---|---|---|---|---|---|---|
| aes-256-gcm | AEAD_AES_256_GCM | 0x0002 | 32 | 12 | No | [NIST-SP-800-38D] | IETF |
| chacha20-poly1305 | AEAD_CHACHA20_POLY1305 | 0x001D | 32 | 12 | No | [RFC8439] | IETF |
| aes-256-gcm-siv | AEAD_AES_256_GCM_SIV | 0x001F | 32 | 12 | Yes | [RFC8452] | IETF |
| aegis-256 | AEAD_AEGIS_256 | 0x0021 | 32 | 32 | No | [I-D.irtf-cfrg-aegis-aead] | IETF |
| aegis-256x2 | AEAD_AEGIS_256X2 | 0x0024 | 32 | 32 | No | [I-D.irtf-cfrg-aegis-aead] | IETF |
The AEAD ID is the uint16 code point entered into both payload_info (Section 5.8.2) and encryption_parameters (Section 5.5); the Binding Name is informative. The AEAD ID values for aes-256-gcm-siv, aegis-256, and aegis-256x2 are early allocations and may change before publication; a change would alter previously written commitments.¶
Nk/Nn are key/nonce sizes in octets. Nn is required to compute segment boundaries (Section 5.8.4). "MRAE" indicates misuse-resistant authenticated encryption ([RFC9771]). Epoch length is not part of this registry: the selected SEAL named instantiation resolves it. See Section 5.8.1 and Section 5.8.3 for details.¶
The aegis-256 and aegis-256x2 Binding Names are the values requested by [I-D.irtf-cfrg-aegis-aead] and are provisional pending IANA action; if IANA assigns different names, this registry follows IANA.¶
IANA is requested to create a SAFE KEM Identifiers registry. This
registry maps SAFE's string identifiers (used in kem= parameters) to
HPKE KEM IDs. Registration policy is Specification Required. Designated
Experts should verify that the KEM is registered in the IANA HPKE KEM
Identifiers registry, is compatible with HPKE export-only mode (AEAD ID
0xFFFF) as specified in Section 5.6.3, that a specification for SPKI
encoding of public keys is provided, and that every field a decoder must
recover from the encoding (notably the KEM ciphertext) is at most 0xFFFE
octets so that it is always encoded literally and never takes the
bind-only digest path (Section 4.1.1). The Auth column indicates whether
the KEM supports AuthEncap/AuthDecap for HPKE Auth mode. Encryptors
MUST NOT include sid or shint with KEMs where Auth=No.¶
Initial entries:¶
| Identifier | HPKE KEM ID | Encap Size | Auth | SPKI Encoding | Change Controller |
|---|---|---|---|---|---|
| x25519 | 0x0020 | 32 octets | Yes | [RFC8410] | IETF |
| p-256 | 0x0010 | 65 octets | Yes | [RFC5480] | IETF |
| ml-kem-768 | 0x0041 | 1088 octets | No | [I-D.ietf-lamps-kyber-certificates] | IETF |
HPKE KEM IDs are defined in the IANA HPKE KEM Identifiers registry established by [RFC9180]. Identifiers MUST consist of lowercase ASCII letters, digits, and hyphens, and MUST NOT exceed 255 octets. Identifier comparison is byte-exact; see Section 5.5.¶
IANA is requested to create a SAFE KDF Identifiers registry. Registration policy is Specification Required. Designated Experts should verify that identifiers consist of lowercase ASCII letters, digits, and hyphens, do not exceed 255 octets, that the underlying KDF provides at least 128-bit security, and that the entry references a KDF registered in the HPKE KDF Identifiers registry (Section 7.2 of [RFC9180]). Per Section 5.5, identifier comparison is byte-exact.¶
Each entry MUST reference a KDF registered in the HPKE KDF Identifiers registry. Each entry MUST include a Binding Name: an informative registry name naming the KDF construction. The value bound into encryption_parameters (Section 5.5) and payload_info is the numeric HPKE KDF ID, not the Binding Name.¶
Two-stage KDFs provide Extract(salt, ikm), Expand(prk, info, L), and Nh. Single-stage KDFs ([I-D.ietf-hpke-pq]) provide Derive(ikm, L).¶
The Nh column specifies the output size in octets used for the commitment and the snapshot authenticator's key. Nh is the KDF's output size: 32 octets for HKDF-SHA-256 and 64 octets for TurboSHAKE-256.¶
The HPKE KDF ID is the uint16 code point entered into both payload_info (Section 5.8.2) and encryption_parameters (Section 5.5); the Binding Name is informative. The Class column determines which SafeDerive instantiation is used (see Section 5.4).¶
The CONFIG field is named "KDF" and its tokens (hkdf-sha-256, turboshake256) are KDF identifiers, because the field selects a KDF (and its underlying hash function) rather than a bare hash algorithm.¶
All conforming implementations MUST implement hkdf-sha-256, which is the default when KDF is omitted from the SAFE CONFIG. Implementations MAY implement turboshake256.¶
Initial entries:¶
| Identifier | Binding Name | HPKE KDF ID | Class | Nh | Reference | Change Controller |
|---|---|---|---|---|---|---|
| hkdf-sha-256 | HKDF-SHA-256 | 0x0001 | two-stage | 32 | [RFC5869] | IETF |
| turboshake256 | TurboSHAKE-256 | 0x0013 | single-stage | 64 | [I-D.ietf-hpke-pq] | IETF |
The hkdf-sha-256 Binding Name, "HKDF-SHA-256", differs from the "HKDF-SHA256" spelling in the HPKE KDF Identifiers registry. The Binding Name is informative and its spelling has no binding consequence; the value entered into encryption_parameters and payload_info is the numeric HPKE KDF ID 0x0001.¶
The turboshake256 HPKE KDF ID 0x0013 is an early allocation per [I-D.ietf-hpke-pq] and may change before that document is published; a change would alter previously written commitments.¶
IANA is requested to create a SAFE Step Names registry. Each registration defines a step type conforming to the interface in Section 5.6. The registry has the following columns:¶
Unique ASCII identifier for the step type (e.g., "pass", "hpke").¶
ABNF grammar for step-specific parameters in the token, or "None" if no parameters.¶
Description of required inputs (e.g., "user passphrase, salt", "recipient private key, kemct").¶
MUST be 32 octets for all registered steps.¶
Document specifying the step's derivation algorithm.¶
Registration policy is Specification Required. Designated Experts MUST verify:¶
The derivation algorithm is deterministic and produces exactly 32 octets¶
Parameter names do not conflict with existing registrations¶
The specification provides complete implementation guidance including the Encode binding form¶
Each field the step requires a decoder to recover from the token is marked as recoverable and is at most 0xFFFE octets, so it is always encoded literally and never takes the bind-only digest path (Section 4.1.1)¶
Step names MUST match the grammar 1*(ALPHA / DIGIT / "-") and MUST NOT
exceed 255 octets. Per Section 5.5, step name comparison
is byte-exact.¶
Initial entries:¶
| Step Name | Parameters Grammar | Inputs | Secret Length | Reference | Change Controller |
|---|---|---|---|---|---|
| pass | kdf, salt, (t, p, m for argon2id; c for pbkdf2) | passphrase, salt | 32 octets | Section 5.6.2 | IETF |
| hpke | kem, kemct, id, (sid, shint for auth) | private key, kemct | 32 octets | Section 5.6.3 | IETF |
The pass step's algorithm variant (argon2id or pbkdf2) is specified in
the step descriptor's kdf parameter. The default KDF is argon2id;
pbkdf2 is available for environments where policy prohibits Argon2id.¶
The hpke step additionally requires the step binding itself as input.
When sid or shint is present, the sender's private key (for
encryption) or public key (for decryption) is also required. Supported
kem values are defined in Section 5.6.3.2; key identifier computation is
defined in Section 5.6.3. hpke(...) with sid or shint is OPTIONAL and
limited to DHKEM-based KEMs (x25519, p-256).¶
Future registrations MAY define additional step types (e.g., hardware token, Oblivious Pseudorandom Function (OPRF)) or variant algorithms for existing step names (subject to Designated Expert review for interoperability impact). A registration request MUST include:¶
Step Name and Parameters Grammar (ABNF)¶
Complete list of Inputs with their sources¶
Derivation algorithm producing exactly 32 octets¶
Definition of any step-specific parameters (name, encoding, semantics)¶
Security considerations for the step type¶
See Appendix I for an illustrative example.¶
IANA is requested to create a SAFE Config Options registry. Each registration defines a CONFIG field name and the registry or value set that defines its legal values. The registry has the following columns:¶
Case-sensitive ASCII field name used in SAFE CONFIG.¶
Registry or fixed set of values allowed for the field.¶
One of "suite" or "representation". A suite option is cryptographically bound through encryption_parameters (Section 5.5) and thus the KEK. A representation option is presentational and binds nothing, selecting only how the SAFE object is serialized.¶
Document specifying the field and its semantics.¶
Registration policy is Specification Required. Designated Experts MUST verify that the field name does not conflict with existing registrations, that the specification defines default behavior when the field is absent, and that the value definition is unambiguous.¶
Initial entries:¶
| Field Name | Value Definition | Scope | Reference | Change Controller |
|---|---|---|---|---|
| AEAD | SAFE AEAD Identifiers registry | suite | Section 10.1 | IETF |
| Profile | SEAL-simple, SEAL-attachment, SEAL-attachment-small, SEAL-editable, SEAL-memory, SEAL-compact | suite | Section 5.8.1 | IETF |
| KDF | SAFE KDF Identifiers registry | suite | Section 10.3 | IETF |
| Lock-Encoding | armored (default), readable | representation | Section 6.2 | IETF |
| Data-Encoding | armored, binary (default) | representation | Section 6.3 | IETF |
The default encoding is "binary" when the Data-Encoding field is omitted. The default Lock-Encoding is "armored" when omitted. The default Profile is SEAL-editable. Profile values are fixed by this specification and [I-D.sullivan-cfrg-raae]; adding new values requires a Standards Track document update.¶
IANA is requested to create a SAFE Block Types registry. This registry lists the block types that may appear in SAFE-encoded data, identified by their fence markers. The registry has the following columns:¶
The block type name as it appears in fence markers (e.g., "CONFIG", "LOCK", "DATA").¶
The opening fence marker string (e.g., "-----BEGIN SAFE CONFIG-----").¶
"Yes" if Decryptors MAY skip unrecognized instances of this block type without failing; "No" if Decryptors MUST fail when encountering an unrecognized block of this type.¶
Document defining the block's semantics.¶
Registration policy is Specification Required. Designated Experts MUST verify:¶
The block type name does not conflict with existing registrations¶
The specification clearly defines the block's syntax and semantics¶
Blocks marked Ignorable=Yes do not affect security or correctness if omitted¶
Block type names MUST consist of uppercase ASCII letters and hyphens and MUST NOT exceed 32 octets.¶
Initial entries:¶
| Block Type | Fence Marker | Ignorable | Reference | Change Controller |
|---|---|---|---|---|
| CONFIG | -----BEGIN SAFE CONFIG----- | No | Section 6.1 | IETF |
| LOCK | -----BEGIN SAFE LOCK----- | No | Section 6.2 | IETF |
| DATA | -----BEGIN SAFE DATA----- | No | Section 6.3.1 | IETF |
All initial block types are critical (Ignorable=No). Future extensions MAY register new block types with Ignorable=Yes for optional features such as detached signatures, metadata, or recipient hints.¶
IANA is requested to register the following media type per [RFC6838]:¶
application¶
safe¶
None¶
None¶
Binary or 7bit. Binary-encoded SAFE data (the default) have ASCII headers followed by raw binary payload data. Armored-encoded SAFE data consist of ASCII printable characters and line feeds, with Base64 encoding for payload data.¶
SAFE-encoded data contain encrypted content. See Section 8 of this document.¶
SAFE headers use PEM-style fence markers; the payload is binary or Base64 armored (Section 6.3). Line wrapping of Base64 content is permitted; Decryptors MUST accept any line length.¶
This document¶
File encryption, secure file sharing, encrypted backups¶
None¶
Deprecated alias names for this type: None¶
Magic number(s): Files begin with "-----BEGIN SAFE" (ASCII)¶
File extension(s): .safe¶
Macintosh file type code(s): None¶
N. Sullivan (nicholas.sullivan+ietf@gmail.com)¶
COMMON¶
None¶
See Authors' Addresses section¶
IETF¶
This appendix is informative.¶
Note: The examples in this section illustrate structure and formatting only. The Base64 values are placeholders and do not represent valid cryptographic outputs. Implementers requiring test vectors with known inputs and outputs should consult the Test Vectors appendix (Appendix G).¶
A minimal object with readable LOCK format (Lock-Encoding set to readable; aes-256-gcm, commitment prefix):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=c2FsdHZhbHVlMTIzNDU2Nzg=) Encrypted-CEK: MTIzNDU2Nzg5MDEyM0FCQ0RFRkdISUpLTE1OT1BRUlNU VVZXWFlaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNA== -----END SAFE LOCK----- -----BEGIN SAFE DATA----- cGxhY2Vob2xkZXJjZWtjb21taXRtZW50aGFzaDEyMzQ1Njc4YWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3 ODkrLz09 -----END SAFE DATA-----¶
A LOCK block encoded as armored (default Lock-Encoding):¶
-----BEGIN SAFE CONFIG----- Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- U3RlcDogcGFzcyhrZGY9YXJnb24yaWQsc2FsdD1jMkZzZEhaaGJIVmxNVEl6TkRV Mk56Zz0pCkVuY3J5cHRlZC1DRUs6IE1USXpORFUyTnpnNU1ERXlNMEZDUTBSRlJr ZElTVXBMVEUxT1QxQlJVbE5VVlZaWFdGbGFZV0pqWkdWbVoyaHBhbXRzYlc1dmNI RnljM1IxZG5kNGVYb3dNVEl6TkE9PQ== -----END SAFE LOCK----- -----BEGIN SAFE DATA----- YmFzZTY0bG9ja2V4YW1wbGVkYXRhY2h1bmtwbGFjZWhvbG RlcmV4YW1wbGVkYXRhMTIzNDU2 -----END SAFE DATA-----¶
HPKE recipient in armored format (derived from the readable example above):¶
-----BEGIN SAFE CONFIG----- Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- U3RlcDogaHBrZShrZW09eDI1NTE5LGlkPVppNm1Rblk1ajhwSVlxM282cldnTnc9 PSxrZW1jdD1ZV0pqWkdWbVoyaHBhbXRzYlc1dmNIRnljM1IxZG5kNGVYb3hNak0w TlRZM09Ea3cpCkVuY3J5cHRlZC1DRUs6IGNHRnpjM2R2Y21ReE1qTTBOVFkzT0Rr d1lXSmpaR1ZtWjJocGFtdHNiVzV2Y0hGeWMzUjFkbmQ0ZVhveE1qTTBOVFkzT0Rr d1lXSmpaR1ZtWjJocGFnPT0= -----END SAFE LOCK----- -----BEGIN SAFE DATA----- aHBrZWV4YW1wbGVjZWtjb21taXRtZW50aGFzaHZhbHVlMTJWR2hwY3lCcGN5QmhJ SE5oYlhCc1pTQmxibU55ZVhCMFpXUWdjR0Y1Ykc5aFpDQjNhWFJvSUcxMWJIUnBj R3hsSUdOb2RXNXJjdz09 -----END SAFE DATA-----¶
A HPKE recipient in readable format (AEAD omitted, uses default aes-256-gcm with commitment prefix):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: hpke(kem=x25519, id=Zi6mQnY5j8pIYq3o6rWgNw==, kemct=YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw) Encrypted-CEK: cGFzc3dvcmQxMjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXoxMjM0NTY3ODkwYWJjZGVmZ2hpag== -----END SAFE LOCK----- -----BEGIN SAFE DATA----- aHBrZWV4YW1wbGVjZWtjb21taXRtZW50aGFzaHZhbHVlMTJWR2hwY3lCcGN5QmhJ SE5oYlhCc1pTQmxibU55ZVhCMFpXUWdjR0Y1Ykc5aFpDQjNhWFJvSUcxMWJIUnBj R3hsSUdOb2RXNXJjdz09 -----END SAFE DATA-----¶
AEGIS-256 with KDF: turboshake256 (non-default AEAD and KDF, 32-octet key identifier, commitment prefix in DATA):¶
-----BEGIN SAFE CONFIG----- AEAD: aegis-256 KDF: turboshake256 Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: hpke(kem=x25519, id=YWVnaXN0dXJib3NoYWtlaWRlbnRpZmllcjMyYnl0ZXM=, kemct=YWVnaXNrZW1jaXBoZXJ0ZXh0ZXhhbXBsZTEyMzQ1Njc4) Encrypted-CEK: YWVnaXNub25jZTEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIz YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2 Nzg5MGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 -----END SAFE LOCK----- -----BEGIN SAFE DATA----- YWVnaXNub2NvbW1pdG1lbnRjaHVua2NpcGhlcnRleHRvbmx5ZXhhbXBsZWRhdGE= -----END SAFE DATA-----¶
Anonymous X25519 recipient (trial decryption required):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: hpke(kem=x25519, kemct=YW5vbnltb3VzZW5jYXBzdWxhdGVka2V5bWF0ZXJpYWwx MjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5vcHFyc3R1dg==) Encrypted-CEK: YW5vbnltb3VzZW5jcnlwdGVkY2VrZGF0YTEyMzQ1Njc4 OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMg== -----END SAFE LOCK----- -----BEGIN SAFE DATA----- YW5vbnltb3VzY2VrY29tbWl0bWVudGhhc2h2YWx1ZXBheWxvYWRjaHVua2RhdGE= -----END SAFE DATA-----¶
Hinted ML-KEM-768 recipient (4-digit hint for candidate filtering):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: hpke(kem=ml-kem-768, hint=4217, kemct=bWxrZW1jaXBoZXJ0ZXh0d2l0aGhpbnRlZHJlY2lwaWVu dGZpbHRlcmluZ2V4YW1wbGVkYXRhMTIzNDU2Nzg5MA==) Encrypted-CEK: aGludGVkZW5jcnlwdGVkY2VrZGF0YWV4YW1wbGUxMjM0 NTY3ODkwYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo= -----END SAFE LOCK----- -----BEGIN SAFE DATA----- aGludGVkY2VrY29tbWl0bWVudGhhc2h2YWx1ZXBheWxvYWRjaHVua2RhdGFoZXJl -----END SAFE DATA-----¶
Two recipients, one passphrase-only and one HPKE (default aes-256-gcm):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=cHdkc2FsdDEyMzQ1Njc4OTA=) Encrypted-CEK: cHdkbm9uY2UxMjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXowMTIzNDU2Nzg5MGFiY2RlZmdoaWo= -----END SAFE LOCK----- -----BEGIN SAFE LOCK----- Step: hpke(kem=p-256, id=Z1d0u6QG0cB2a4nM3Kp2Ww==, kemct=QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkwYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXo=) Encrypted-CEK: aHBrZW5vbmNlMTIzNDU2Nzg5MGFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6MDEyMzQ1Njc4OTBhYmNkZWZnaGlq -----END SAFE LOCK----- -----BEGIN SAFE DATA----- bXVsdGlyZWNpcGllbnRjZWtjb21taXRtZW50aGFzaHZhbHVlVFhWc2RHa3RjbVZq YVhCcFpXNTBJR1Y0WVcxd2JHVWdkMmwwYUNCemFHRnlaV1FnY0dGNWJHOWhaQ0Js Ym1OeWVYQjBaV1FnYjI1alpRPT0= -----END SAFE DATA-----¶
Multi-step sequence with AND semantics (passphrase AND HPKE, chacha20-poly1305 with commitment prefix):¶
-----BEGIN SAFE CONFIG----- AEAD: chacha20-poly1305 Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=bXVsdGlzdGVwc2FsdDEyMzQ=) Step: hpke(kem=x25519, id=eEF3bXlyT3BWbXpLUjRCdz09, kemct=bXVsdGlzdGVwZXhhbXBsZWtlbWNpcGhlcnRleHQxMjM0NTY3) Encrypted-CEK: bXVsdGlzdGVwbm9uY2UxMjM0NTY3ODkwYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5MGFiYw== -----END SAFE LOCK----- -----BEGIN SAFE DATA----- bXVsdGlzdGVwY2VrY29tbWl0bWVudGhhc2h2YWx1ZTEyMzQ1VW1WeGRXbHlaWE1n WW05MGFDQndZWE56ZDI5eVpDQkJUa1FnV0RJMU5URTVJSEJ5YVhaaGRHVWdhMlY1 SUhSdklHUmxZM0o1Y0hRPQ== -----END SAFE DATA-----¶
Note: The armored LOCK examples above use placeholder Base64 values. The armored payload is the Base64 of the readable LOCK per Section 6.2.2.¶
Two-passphrase step sequence (requires both passphrases to decrypt):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=Zmlyc3RwYXNzd29yZHNhbHQ=) Step: pass(kdf=pbkdf2, salt=c2Vjb25kcGFzc3dvcmRzYWx0) Encrypted-CEK: dHdvcGFzc3dvcmRub25jZTEyMzQ1Njc4OTBhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkwYWI= -----END SAFE LOCK-----¶
Hybrid post-quantum step sequence (X25519 AND ML-KEM-768, default aes-256-gcm with commitment prefix):¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: hpke(kem=x25519, id=cHF6RmlucHJpbnQxMjM0NTY3, kemct=eDI1NTE5a2VtY2lwaGVydGV4dDEyMzQ1Njc4OTBhYmNkZWY=) Step: hpke(kem=ml-kem-768, id=bWxrZW1maW5nZXJwcmludDEyMw==, kemct=bWxrZW03NjhrZW1jaXBoZXJ0ZXh0ZXh0cmVtZWx5bG9uZ2Jh c2U2NGVuY29kZWRkYXRhYXBwcm94aW1hdGVseTEwODhvY3Rl dHNmb3JwcXNlY3VyaXR5dGhpc2lzZHVtbXlkYXRhZm9yZGVt ... NzY4a2VtY2lwaGVydGV4dGVuY2Fwc3VsYXRpb25kYXRh) Encrypted-CEK: aHlicmlkbm9uY2UxMjM0NTY3ODkwYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXowMTIzNDU2Nzg5MGFiY2RlZmc= -----END SAFE LOCK----- -----BEGIN SAFE DATA----- aHlicmlkcHFjZWtjb21taXRtZW50aGFzaHZhbHVlMTIzNDU2VUc5emRDMXhkV0Z1 ZEhWdElHaDVZbkpwWkNCbGVHRnRjR3hsSUdSbGJXOXVjM1J5WVhScGJtY2dZMjl0 WW1sdVpXUWdXREkxTlRFNUlHRnVaQ0JOVEMxTFJVMHQ= -----END SAFE DATA-----¶
This appendix is informative. For a concise summary of the encryptor and decryptor flows, see Section 3.¶
Encryptor processing proceeds in three phases: setup, recipient key wrapping, and content encryption.¶
During setup, the Encryptor:¶
Selects a Profile and AEAD algorithm (or accepts defaults)¶
Emits a SAFE CONFIG block if using non-default values¶
Generates a random 32-octet CEK: SafeRandom(32, "SAFE-CEK")¶
Generates a 32-octet salt: SafeRandom(32, "SAFE-SALT")¶
Encryptors should generate a fresh CEK for each file. When the same CEK is reused across files, the per-file salt ensures payload keys are unique: each salt produces distinct commitment, payload_key, snap_key, and nonce_base values via Section 5.8.2.¶
For each recipient, the Encryptor wraps the CEK by:¶
Emitting Step lines with required step parameters (salt for
pass steps, kemct for hpke steps)¶
Generating a fresh lock_nonce: SafeRandom(Nn, "SAFE-LOCK-NONCE")¶
Deriving step secrets and computing the KEK per Section 5.7.1¶
Emitting the Encrypted-CEK field¶
To encrypt content, the Encryptor:¶
Derives payload_key from the CEK and payload_info (which includes the per-file salt) per Section 5.8.2¶
Splits the plaintext into Segment-Size segments (N segments total)¶
Encrypts each segment per the SEAL segment procedure (Section 4.8 of [I-D.sullivan-cfrg-raae])¶
What the Encryptor stores per segment follows the resolved nonce mode and snap_id (Section 5.8.4). Stored metadata is placed per the layout:¶
Linear layout (Section 6.4.1): each encrypted segment is its ciphertext and tag, preceded by the nonce in random mode.¶
Aligned layout (Section 6.4.2): the header metadata array holds each segment's entry (Section 5.8.4), and ciphertext segments are at aligned offsets.¶
Decryptor processing proceeds in three phases: configuration, CEK recovery, and content decryption.¶
During configuration, the Decryptor reads the SAFE CONFIG (if present) to learn non-default values and constructs encryption_parameters from the Profile, AEAD, and KDF per Section 5.5.¶
To recover the CEK, the Decryptor:¶
Selects a LOCK block per Section 6.2.3¶
Parses lock_nonce (first Nn octets) from the Encrypted-CEK field¶
Evaluates steps to derive the KEK per Section 5.7.1¶
Decrypts Encrypted-CEK to recover the 32-octet CEK¶
Encrypted-CEK size validation is specified in Section 6.2.¶
To decrypt content, the Decryptor reads the 32-octet salt leading the raAE header (Section 6.4), then derives payload_key from the CEK and payload_info (which includes that salt) per Section 5.8.2. It then decrypts each segment per the SEAL segment procedure (Section 4.8 of [I-D.sullivan-cfrg-raae]). The resolved nonce mode (Section 5.8.1) governs the nonce source. In random mode (nonce_mode 0x00) the Decryptor reads each nonce from the stored metadata. In derived mode (nonce_mode 0x01) it recomputes the nonce from the key schedule (Section 5.8.4). The location of the stored components depends on the layout:¶
Linear layout (Section 6.4.1): each encrypted segment contains its ciphertext and tag, preceded by the nonce in random mode.¶
Aligned layout (Section 6.4.2): the header metadata array holds each segment's entry (Section 5.8.4), and ciphertext segments are at aligned offsets.¶
See Appendix E for offset calculations.¶
This appendix is informative.¶
For interoperability testing, implementations may use the following error identifiers to categorize failures:¶
| Error Code | Description | When to Emit |
|---|---|---|
| ERR_UNSUPPORTED_AEAD | Unknown AEAD algorithm | Parsing SAFE CONFIG |
| ERR_UNSUPPORTED_KEM | Unknown KEM identifier | Parsing hpke(...) step |
| ERR_INVALID_PROFILE | Invalid Profile value | Parsing SAFE CONFIG |
| ERR_HPKE_NO_MATCH | No matching private key | Recipient discovery |
| ERR_HPKE_DECAP_FAILED | HPKE decapsulation error | CEK recovery |
| ERR_LOCK_AEAD_FAILED | Encrypted-CEK decryption failed | CEK recovery |
| ERR_PAYLOAD_AEAD_FAILED | Segment decryption failed | Content decryption |
| ERR_SEGMENT_OUT_OF_RANGE | Segment index invalid | Content decryption |
| ERR_MALFORMED_BASE64 | Base64 decoding error | Any Base64 field |
| ERR_DUPLICATE_FIELD | Repeated field name | Parsing SAFE CONFIG |
| ERR_DUPLICATE_PARAM | Repeated step parameter | Parsing Step lines |
| ERR_MISSING_SALT | pass(...) without salt
|
Parsing LOCK |
| ERR_MISSING_KEMCT | hpke(...) without kemct
|
Parsing LOCK |
| ERR_MULTIPLE_PASS_ONLY_LOCK | Multiple same-variant pass-only LOCKs | Discovery |
| ERR_NON_ASCII_HEADER | Non-ASCII in header | Parsing any header |
| ERR_RESOURCE_LIMIT | Size/count limit exceeded | Parsing any block |
| ERR_INVALID_SALT_LENGTH | salt not exactly 32 octets | Parsing salt
|
| ERR_COMMITMENT_MISMATCH | Commitment verification failed | Key schedule |
| ERR_SNAPSHOT_MISMATCH | Snapshot verification failed | Integrity check |
| ERR_TRUNCATION | Missing final segment or segment with is_final=0 at EOF | Content decryption |
This appendix is informative.¶
In armored mode, Decryptors compute the segment count N and final segment size from the Base64 payload length.¶
Let S_b64 be the payload string between the fences, len_b64 its
length in characters, pad the number of trailing = signs (0, 1,
or 2), and len_bin_total = 3 * floor(len_b64 / 4) - pad.¶
The header region is 32 (salt) + Nh (commitment) + Na octets, where Na is the snapshot value length: 2*Nh at snap_id 0x0001 and 0 at snap_id 0x0000 (Section 5.8.7). The encrypted segment region is len_bin_ciphertext = len_bin_total - 32 - Nh - Na.¶
Let B = Segment-Size and Nn = AEAD nonce length. In derived mode (nonce_mode 0x01) nonces are not stored, so the per-segment overhead excludes Nn. Define:¶
if nonce_mode == 0x01: # derived mode
C = B + 16
C_min = 16 # minimum: tag only
else: # random mode (0x00)
C = Nn + B + 16
C_min = Nn + 16 # minimum: nonce + tag
N_nonfinal = floor( len_bin_ciphertext / C )
rem = len_bin_ciphertext - N_nonfinal * C
if rem == 0:
N = N_nonfinal
C_final = C
else if rem < C_min:
reject as malformed
else:
N = N_nonfinal + 1
C_final = rem
if nonce_mode == 0x01: # derived mode
L_final = C_final - 16
else: # random mode (0x00)
L_final = C_final - Nn - 16
¶
A decryptor decrypting segment index i computes octet offsets relative to the start of the encrypted segment region (after the salt, commitment, and snapshot value, when present):¶
seg_byte_start = i * C seg_byte_len = C if i < N - 1 else C_final byte_start = 32 + Nh + Na + seg_byte_start byte_len = seg_byte_len char_start = 4 * floor( byte_start / 3 ) char_end = 4 * ceil( (byte_start + byte_len) / 3 )¶
For each segment index i, the Decryptor:¶
Extracts S_b64[char_start:char_end]¶
Base64-decodes to a temporary buffer tmp¶
Computes skip = byte_start mod 3¶
Selects encrypted_segment = tmp[skip : skip + byte_len]¶
In random mode (nonce_mode 0x00): parses
nonce_i = encrypted_segment[0:Nn] and ciphertext_i =
encrypted_segment[Nn:]. In derived mode (nonce_mode 0x01):
ciphertext_i = encrypted_segment and nonce_i is recomputed per
Section 5.8.4¶
Determines is_final = 1 if i == N - 1 else 0¶
Constructs data_aad(i, is_final) per Section 5.8.8¶
Computes key_i = segment_key(i) (Section 5.8.3)¶
AEAD-opens ciphertext_i under key_i with nonce_i and data_aad¶
This appendix is informative.¶
To decrypt segment index i from a long object, a Decryptor first selects a candidate LOCK per Section 6.2.3. The decryptor constructs encryption_parameters (aead_id, Segment-Size, kdf_id, snap_id, epoch_length_u8) from the SAFE CONFIG or from defaults, parses lock_nonce from Encrypted-CEK, evaluates the step sequence to derive the KEK, and opens Encrypted-CEK to recover the CEK. The decryptor reads the 32-octet salt leading the raAE header (Section 6.4), then derives payload_key and snap_key from the CEK and payload_info (which includes that salt). It then locates segment i in the payload. No other segments need to be read or decoded. What the returned plaintext carries is the per-segment case of Section 5.8.7. Under snap_id 0x0001 it carries per-segment authenticity but not the whole-object snapshot guarantee, which there is verifiable only over the whole segment set. Under 0x0002 and 0x0003 the Decryptor covers that one segment by the authenticator's per-segment procedure (Section 5.8.7), so the returned plaintext carries snapshot integrity too (Section 6.4.2).¶
Under an aligned-layout Profile, read N and D from the header, then compute segment i's ciphertext offset as (D + i) × B. Segment i's metadata entry begins at header_len + 8 (N and D) + 32 (salt) + Nh (commitment) + Na (the snapshot value), plus E × Nh when snap_id is 0x0003, plus i × meta_len. Section 6.4.2 gives Na, E, and meta_len.¶
Under a linear-layout Profile the segments follow the raAE header in order, so segment i's ciphertext begins at header_len + 32 (salt) + Nh (commitment) + Na (the snapshot value) + i * C, where C is the per-segment stored size: Nn + B + 16 in random mode and B + 16 in derived mode. Under armored encoding the Decryptor must additionally compute the Base64 character window covering that range and decode only that window, as described below.¶
Armored encoding is defined only over the linear layout (Section 6.3), so this example uses SEAL-editable, whose nonce mode is random (nonce_mode 0x00) and whose Segment-Size is 65536 (Section 5.8.1), with AES-256-GCM (Nn=12). Under a Profile whose nonce mode is derived, omit Nn from the per-segment sizes (C = B + 16 instead of Nn + B + 16).¶
Consider three segments: two full segments plus a 5000-octet final segment.¶
C = Nn + B + 16 = 12 + 65536 + 16 = 65564 octets (full segment) C_final = Nn + 5000 + 16 = 12 + 5000 + 16 = 5028 octets (final segment) total_binary = 32 + 32 + 64 + 65564 + 65564 + 5028 = 136284 octets Base64 len = ceil(136284 / 3) * 4 = 181712 characters¶
To decrypt segment i=0, compute octet and character offsets (salt, commitment, and the snapshot value occupy the first 128 octets):¶
byte_start = 128 + (0 * C) = 128 byte_len = C = 65564 char_start = floor(byte_start / 3) * 4 = 168 char_end = ceil((byte_start + byte_len) / 3) * 4 = 87592 skip = byte_start mod 3 = 2¶
Extract S_b64[168:87592], Base64-decode, then take 65564 octets as the
encrypted segment starting at skip = 2 (discard the leading two octets).
In random mode (nonce_mode 0x00), parse the first Nn octets (12 for
AES-256-GCM) as the stored nonce, compute segment_key(i)
(Section 5.8.3), and AEAD-open the remaining octets under
segment_key(i) with the extracted nonce and segment AAD. In derived
mode (nonce_mode 0x01), derive the nonce from nonce_base instead
(Section 5.8.4).¶
This appendix is informative.¶
SAFE's design choices reflect trade-offs between flexibility, performance, and simplicity. This section explains the rationale behind key architectural decisions.¶
SAFE separates the Content-Encryption Key (CEK) from the Key-Encryption Key (KEK) to enable multi-recipient encryption without duplicating payload ciphertexts.¶
A single CEK is generated once and used to encrypt the payload; each recipient's LOCK derives a KEK that wraps the same CEK. This design offers several advantages:¶
Storage and bandwidth efficiency: Adding recipients requires only adding LOCK blocks (typically < 1 KB each), not duplicating the entire payload. For large files, this is critical.¶
Key rotation: Recipients can be added or removed by re-wrapping the CEK under new KEKs without re-encrypting the payload.¶
Operational flexibility: The CEK remains constant while KEKs rotate, simplifying key management.¶
This design implies that all recipients share the same payload_key. Encryptors who require per-recipient payload keys (e.g., for fine-grained access control that survives CEK compromise) would need to encrypt multiple independent payloads.¶
If recipients directly decrypted the payload with their KEK, each recipient would require a distinct copy of the ciphertext, multiplying storage and bandwidth costs.¶
In random nonce mode, segment associated data is Encode("SEAL-DATA",
uint64(i), uint8(is_final)), binding each segment to its position and
finality. In derived nonce mode the index and finality are bound in the
nonce instead and omitted from the AAD (Section 5.8.8 is authoritative).
Either way, suite parameters and LOCK-specific data are excluded from
segment AAD.¶
This choice prioritizes simplicity and O(1) random access:¶
Selective decryption: payload_key already depends on payload_info, so segment AAD need not repeat the negotiated parameters. This avoids requiring every segment decryption to reference the SAFE CONFIG.¶
Multi-recipient caching: Including LOCK-specific data (Step lines, kemct) would couple segment decryption to a specific LOCK, preventing efficient caching of payload_key across multiple recipients.¶
Suite and LOCK binding is indirect through the key hierarchy:¶
The KEK schedule binds encryption_parameters and all step_bindings in one SafeDerive call. Encrypted-CEK AEAD authenticates the CEK under this KEK.¶
The payload schedule binds payload_key to payload_info via SealDerive.¶
Each segment is bound to its index and finality flag, preventing reordering, splicing, truncation, and extension within a file (Section 5.8.8).¶
The snapshot authenticator (Section 5.8.7) binds every segment's per-segment input and the segment count, providing file-level integrity without per-segment decryption. Its construction and security are specified in Section 4.7 of [I-D.sullivan-cfrg-raae] (Snapshot Authenticator).¶
An alternative design could include a "recipient_id" in segment AAD, but this would require additional per-recipient metadata and complicate multi-recipient scenarios. SAFE's choice favors performance and simplicity for the common case of single-recipient or trust-equivalent multi-recipient files, while accepting that ciphertext segments alone do not directly identify which LOCK unlocked them.¶
SAFE provides built-in truncation and extension detection via the is_final flag bound to each segment (Section 5.8.8). The final segment is marked with is_final=1, and all preceding segments use is_final=0. This design, inspired by the STREAM construction [STREAM], enables:¶
Truncation detection: a segment with is_final=0 and no successor indicates truncation¶
Extension prevention: appending after is_final=1 fails AEAD verification¶
Streaming writes: encryptors buffer the last segment until the stream closes, then encrypt it with is_final=1¶
Per-segment random nonces (Section 5.8.4) enable selective editing: individual segments can be re-encrypted without affecting LOCK blocks or other segments. This design trades a small storage overhead (Nn octets per segment) for flexibility in payload modification.¶
SafeDerive (Section 5.4.1) uses a fixed string ("SAFE-v1") as the HKDF salt in its Extract step. The same string appears in the Encode-framed Input Keying Material (IKM), making its presence in the salt redundant but harmless. This design is intentional: the fixed salt binds every Extract call to the protocol version at the HMAC key position, while security relies on the entropy of the IKM, not the salt. All security-critical IKMs (CEK, step secrets, hedged random values) have sufficient min-entropy for this to hold. Krawczyk's analysis of HKDF shows that Extract with a fixed salt is a good randomness extractor when the IKM has high min-entropy. For the XOF single-stage variant, there is no separate salt; the version string appears only within the Encode frame.¶
This appendix is informative. It collects known-answer tests and derivation vectors for SAFE. All values are hex unless noted.¶
This subsection is a complete known-answer test for a passphrase-based SAFE encoding using the default SEAL-editable Profile (aes-256-gcm, segment size 65536, hkdf-sha-256, random nonces, snapshot on) with Data-Encoding armored.¶
## Inputs
Passphrase: "correct horse battery staple" (UTF-8, 28 octets)
Salt: 01010101010101010101010101010101
CEK: aaaaaaaa...aa (32 octets of 0xAA)
lock_nonce: 020202020202020202020202
payload_salt: 0404...0404 (32 octets of 0x04)
Plaintext: "Hello, SAFE!" (12 octets)
seg0 nonce: 030303030303030303030303 (stored random nonce)
## encryption_parameters (5 elements, KEK-bound)
encryption_parameters = [
aead_id: 0002 (AEAD_AES_256_GCM)
segment_size_be: 00010000 (= I2OSP(65536, 4))
kdf_id: 0001 (HKDF-SHA-256)
snap_id: 0001 (snapshot on)
epoch_length_u8: 10 (r = 16, SEAL-editable Profile)
]
payload_info additionally carries, before the salt:
nonce_mode: 00 (random)
## LOCK Key Derivation
step_binding:
00047061737300086172676f6e326964
00100101010101010101010101010101
0101
Encode("pass", "argon2id", salt)
step_secret:
7d3491ac8af1b54526792869b7257f5d
bf7cc3c20929417bb193e396c51d7965
DerivePasswordStepSecret(argon2id)
derived_kek:
edc057d21862641949fbdc1607f8b2b2
d8e85eca45b82d9fc100c53e94623114
SafeDerive("lock-kek", [step_secret],
[...encryption_parameters, step_binding], Nk)
## Encrypted-CEK
aad = "" (empty)
Encrypted-CEK:
02020202020202020202020206265af6
e8060e7a7522dafb2d35697599a8f935
30178851ff8381916e05cd8a09202873
4f1d066269d6a7f311b86bb7
lock_nonce || AEAD.Seal(KEK, lock_nonce, CEK)
## Payload Schedule
payload_info = [aead_id, segment_size_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt] (7 elements)
payload_salt:
04040404040404040404040404040404
04040404040404040404040404040404
commitment:
c09e0f51871d431bc5624676212acf76
c8239ea5c8e54cd6b225d271122c4e8e
SealDerive("commit", CEK, [...payload_info, G], 32)
payload_key:
17f41951e5551ebf40eb781455df28f4
3ec6f249f1201892d3479f5e1ca500c4
SealDerive("payload_key", CEK, payload_info, 32)
snap_key:
b2aaae9c93648106d48c98063f310dc6
b227aef1aeee6e1a36515ea359dcd2e3
SealDerive("acc_key", CEK, payload_info, 32)
## Segment 0 (is_final=1)
nonce_0:
030303030303030303030303
data_aad_0:
00095345414c2d444154410008000000
0000000000000101
Encode("SEAL-DATA", uint64(0), uint8(1))
segment_key_0:
5d437730ddfc2e313028ea4ad3b346bf
fac22e0684a6721c388e6de66e9a1ca8
epoch_key(0) = SealDerive("epoch_key",
payload_key, [uint64(0)], 32)
ciphertext+tag_0:
638bdea85af8bee96102a9d4aad21177
de9020008d812a702619700d
tag_0:
aad21177de9020008d812a702619700d
encrypted_block_0:
030303030303030303030303638bdea8
5af8bee96102a9d4aad21177de902000
8d812a702619700d
## Snapshot (snap_id 0x0001)
contrib_0:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
accumulator:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
= contrib_0 (single segment)
snapshot_tag:
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
SnapTag: SealDerive("snapshot_tag", snap_key,
[uint64(1), accumulator], 32)
snapshot (Na=64) = wrapped_acc || snapshot_tag:
snapshot:
46db8d79155f6c25857b09f4b2230fc5
8328044dd548e66442fe67b893da7e76
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
wrapped_acc = accumulator XOR
snapmask(n_seg, snapshot_tag)
## Complete DATA object (168 octets)
DATA = salt(32) || commitment(32) || snapshot(64)
|| [ nonce(12) || ct+tag per segment ]
= 168 octets, 224 Base64 chars
¶
Readable format: ~~~~ -----BEGIN SAFE CONFIG----- Lock-Encoding: readable Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: pass(kdf=argon2id, salt=AQEBAQEBAQEBAQEBAQEBAQ==) Encrypted-CEK: AgICAgICAgICAgICBiZa9ugGDnp1Itr7LTVpdZmo+TUwF4hR/4OBkW4FzYoJIChz Tx0GYmnWp/MRuGu3 -----END SAFE LOCK----- -----BEGIN SAFE DATA----- BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBATAng9Rhx1DG8ViRnYhKs92 yCOepcjlTNayJdJxEixOjkbbjXkVX2wlhXsJ9LIjD8WDKARN1UjmZEL+Z7iT2n52 V6Z4ZefuLRUt0s9Cc4GoXDegCghjMG/JMlXHAmYxhc4DAwMDAwMDAwMDAwNji96o Wvi+6WECqdSq0hF33pAgAI2BKnAmGXAN -----END SAFE DATA----- ~~~~¶
Same object, armored LOCK (default Lock-Encoding): ~~~~ -----BEGIN SAFE CONFIG----- Data-Encoding: armored -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- ACIABHBhc3MACGFyZ29uMmlkABABAQEBAQEBAQEBAQEBAQEBADwCAgICAgICAgIC AgIGJlr26AYOenUi2vstNWl1maj5NTAXiFH/g4GRbgXNigkgKHNPHQZiadan8xG4 a7c= -----END SAFE LOCK----- -----BEGIN SAFE DATA----- BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBATAng9Rhx1DG8ViRnYhKs92 yCOepcjlTNayJdJxEixOjkbbjXkVX2wlhXsJ9LIjD8WDKARN1UjmZEL+Z7iT2n52 V6Z4ZefuLRUt0s9Cc4GoXDegCghjMG/JMlXHAmYxhc4DAwMDAwMDAwMDAwNji96o Wvi+6WECqdSq0hF33pAgAI2BKnAmGXAN -----END SAFE DATA----- ~~~~¶
This appendix is informative.¶
A single X25519 recipient using DHKEM(X25519, HKDF-SHA256), with the recipient keys from [RFC9180] Appendix A.7.1 and the default SEAL-editable Profile. All values are hex unless noted.¶
## Inputs
Recipient skR:
4612c550263fc8ad58375df3f557aac5
31d26850903e55a9f23f21d8534e8ac8
Recipient pkR:
3948cfe0ad1ddb695d780e59077195da
6c56506b027329794ab02bca80815c4d
Ephemeral skE:
52c4a758a802cd8b936eceea31443279
8d5baf2d7e9235dc084ab1b9cfa2f736
Ephemeral pkE (= enc):
37fda3567bdbd628e88668c3c8d7e97d
1d1253b6d4ea6d44c150f741f1bf4431
CEK: aaaaaaaa...aa (32 octets of 0xAA)
lock_nonce: 020202020202020202020202
payload_salt: 0404...0404 (32 octets of 0x04)
Plaintext: "Hello, SAFE!" (12 octets)
seg0 nonce: 030303030303030303030303 (stored random nonce)
## encryption_parameters (5 elements, KEK-bound)
encryption_parameters = [
aead_id: 0002 (AEAD_AES_256_GCM)
segment_size_be: 00010000 (= I2OSP(65536, 4))
kdf_id: 0001 (HKDF-SHA-256)
snap_id: 0001 (snapshot on)
epoch_length_u8: 10 (r = 16, SEAL-editable Profile)
]
payload_info additionally carries, before the salt:
nonce_mode: 00 (random)
## DHKEM(X25519, HKDF-SHA256)
dh = DH(skE, pkR):
b3b5c19eab3f088ac18f23f774ff6414
ba4fde45404d10085efc3e4dc9c72e35
kem_context = enc || pkR:
37fda3567bdbd628e88668c3c8d7e97d
1d1253b6d4ea6d44c150f741f1bf4431
3948cfe0ad1ddb695d780e59077195da
6c56506b027329794ab02bca80815c4d
shared_secret:
fe0e18c9f024ce43799ae393c7e8fe8f
ce9d218875e8227b0187c04e7d2ea1fc
## SAFE Step Secret (base mode)
key_id (id):
98cdd10b776ac15ed78f5520bed9f3e6
ffdf682fe3ecb68163b4f1dd8b1dfefa
SafeDerive("SAFE-SPKI-v1", [SPKI(pkR)], [""], 32)
step_binding:
000468706b6500067832353531390020
37fda3567bdbd628e88668c3c8d7e97d
1d1253b6d4ea6d44c150f741f1bf4431
002098cdd10b776ac15ed78f5520bed9
f3e6ffdf682fe3ecb68163b4f1dd8b1d
fefa
Encode("hpke", "x25519", enc, id)
step_secret:
42a4a3f299e1a71a97b04a3d9a7e9ae6
7cd1b8ea3dec017e26fa1e369ee6f85b
HPKE Base export over the shared_secret
## LOCK Key Derivation
derived_kek:
33256ae6986d734cab5564a7f69af526
b7f2a233b6524fb07575a9ab2fc0e01e
SafeDerive("lock-kek", [step_secret],
[...encryption_parameters, step_binding], Nk)
## Encrypted-CEK
Encrypted-CEK:
02020202020202020202020275e7823b
d597c54b038f701123cd1e766936d5ea
40a86d5698d492570403b93f90617043
74b370c7bee30f74fb194cc5
## Payload Schedule
payload_info = [aead_id, segment_size_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt] (7 elements)
payload_salt:
04040404040404040404040404040404
04040404040404040404040404040404
commitment:
c09e0f51871d431bc5624676212acf76
c8239ea5c8e54cd6b225d271122c4e8e
SealDerive("commit", CEK, [...payload_info, G], 32)
payload_key:
17f41951e5551ebf40eb781455df28f4
3ec6f249f1201892d3479f5e1ca500c4
SealDerive("payload_key", CEK, payload_info, 32)
snap_key:
b2aaae9c93648106d48c98063f310dc6
b227aef1aeee6e1a36515ea359dcd2e3
SealDerive("acc_key", CEK, payload_info, 32)
## Segment 0 (is_final=1)
nonce_0:
030303030303030303030303
data_aad_0:
00095345414c2d444154410008000000
0000000000000101
Encode("SEAL-DATA", uint64(0), uint8(1))
segment_key_0:
5d437730ddfc2e313028ea4ad3b346bf
fac22e0684a6721c388e6de66e9a1ca8
epoch_key(0) = SealDerive("epoch_key",
payload_key, [uint64(0)], 32)
ciphertext+tag_0:
638bdea85af8bee96102a9d4aad21177
de9020008d812a702619700d
tag_0:
aad21177de9020008d812a702619700d
encrypted_block_0:
030303030303030303030303638bdea8
5af8bee96102a9d4aad21177de902000
8d812a702619700d
## Snapshot (snap_id 0x0001)
contrib_0:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
accumulator:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
= contrib_0 (single segment)
snapshot_tag:
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
SnapTag: SealDerive("snapshot_tag", snap_key,
[uint64(1), accumulator], 32)
snapshot (Na=64) = wrapped_acc || snapshot_tag:
snapshot:
46db8d79155f6c25857b09f4b2230fc5
8328044dd548e66442fe67b893da7e76
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
wrapped_acc = accumulator XOR
snapmask(n_seg, snapshot_tag)
## Complete DATA object (168 octets)
DATA = salt(32) || commitment(32) || snapshot(64)
|| [ nonce(12) || ct+tag per segment ]
= 168 octets, 224 Base64 chars
¶
This appendix is informative.¶
Auth-mode HPKE with a single X25519 recipient, using the same recipient keys as the X25519 vector and the default SEAL-editable Profile. All values are hex unless noted.¶
## Inputs
Recipient skR:
4612c550263fc8ad58375df3f557aac5
31d26850903e55a9f23f21d8534e8ac8
Recipient pkR:
3948cfe0ad1ddb695d780e59077195da
6c56506b027329794ab02bca80815c4d
Sender skS:
6b7298af684f45181f80ac5cb3d9a371
3abb62cbecd21db5dba0eb2a8bfb3a05
Sender pkS:
ccc340219b8098b48749f1c36e2c336f
aefb87f9cbe3463e59e3b8ec18c44c49
Ephemeral skE:
52c4a758a802cd8b936eceea31443279
8d5baf2d7e9235dc084ab1b9cfa2f736
Ephemeral pkE (= enc):
37fda3567bdbd628e88668c3c8d7e97d
1d1253b6d4ea6d44c150f741f1bf4431
CEK: aaaaaaaa...aa (32 octets of 0xAA)
lock_nonce: 020202020202020202020202
## encryption_parameters (5 elements, KEK-bound)
encryption_parameters = [
aead_id: 0002 (AEAD_AES_256_GCM)
segment_size_be: 00010000 (= I2OSP(65536, 4))
kdf_id: 0001 (HKDF-SHA-256)
snap_id: 0001 (snapshot on)
epoch_length_u8: 10 (r = 16, SEAL-editable Profile)
]
payload_info additionally carries, before the salt:
nonce_mode: 00 (random)
## DHKEM Auth Mode (X25519)
shared_secret:
8c1cd5a4e40708af36bf6446d65f62ed
7a7b7f1286d966d9bc96b96808021ba8
## SAFE Step Secret (Auth mode)
id:
98cdd10b776ac15ed78f5520bed9f3e6
ffdf682fe3ecb68163b4f1dd8b1dfefa
SafeDerive("SAFE-SPKI-v1", [SPKI(pkR)], [""], 32)
sid:
d9b9d59d0f10a55a8365b2f440dbf787
f280e0f400427beaaf17de1f8662fcdc
SafeDerive("SAFE-SPKI-v1", [SPKI(pkS)], [""], 32)
step_binding:
000468706b6500067832353531390020
37fda3567bdbd628e88668c3c8d7e97d
1d1253b6d4ea6d44c150f741f1bf4431
002098cdd10b776ac15ed78f5520bed9
f3e6ffdf682fe3ecb68163b4f1dd8b1d
fefa0004617574680020d9b9d59d0f10
a55a8365b2f440dbf787f280e0f40042
7beaaf17de1f8662fcdc
Encode("hpke", "x25519", enc, id, "auth", sid)
step_secret:
24c50a152128baaa3f77c255b0e5b895
2cece2e7680b21bd7dc24aa1807e9374
HPKE Auth export over the shared_secret
## LOCK Key Derivation
derived_kek:
c69b80d05f8e61938af6935b6e91ad90
3a675c94f927d17cf8e2e35961c25522
## Encrypted-CEK
Encrypted-CEK:
0202020202020202020202029f79d2aa
42b0b4ae4f055655abaf041fd8d697ca
ec9d493e86bbd25c16c2e3bc1654050d
923d573048785e94cebe6d91
## Payload Schedule
payload_info = [aead_id, segment_size_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt] (7 elements)
payload_salt:
04040404040404040404040404040404
04040404040404040404040404040404
commitment:
c09e0f51871d431bc5624676212acf76
c8239ea5c8e54cd6b225d271122c4e8e
SealDerive("commit", CEK, [...payload_info, G], 32)
payload_key:
17f41951e5551ebf40eb781455df28f4
3ec6f249f1201892d3479f5e1ca500c4
SealDerive("payload_key", CEK, payload_info, 32)
snap_key:
b2aaae9c93648106d48c98063f310dc6
b227aef1aeee6e1a36515ea359dcd2e3
SealDerive("acc_key", CEK, payload_info, 32)
## Segment 0 (is_final=1)
nonce_0:
030303030303030303030303
data_aad_0:
00095345414c2d444154410008000000
0000000000000101
Encode("SEAL-DATA", uint64(0), uint8(1))
segment_key_0:
5d437730ddfc2e313028ea4ad3b346bf
fac22e0684a6721c388e6de66e9a1ca8
epoch_key(0) = SealDerive("epoch_key",
payload_key, [uint64(0)], 32)
ciphertext+tag_0:
638bdea85af8bee96102a9d4aad21177
de9020008d812a702619700d
tag_0:
aad21177de9020008d812a702619700d
encrypted_block_0:
030303030303030303030303638bdea8
5af8bee96102a9d4aad21177de902000
8d812a702619700d
## Snapshot (snap_id 0x0001)
contrib_0:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
accumulator:
ebd45d826cff550730838787dc361e9e
553f6bb006c8486aea1be12ffefb32aa
= contrib_0 (single segment)
snapshot_tag:
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
SnapTag: SealDerive("snapshot_tag", snap_key,
[uint64(1), accumulator], 32)
snapshot (Na=64) = wrapped_acc || snapshot_tag:
snapshot:
46db8d79155f6c25857b09f4b2230fc5
8328044dd548e66442fe67b893da7e76
57a67865e7ee2d152dd2cf427381a85c
37a00a0863306fc93255c702663185ce
wrapped_acc = accumulator XOR
snapmask(n_seg, snapshot_tag)
## Complete DATA object (168 octets)
DATA = salt(32) || commitment(32) || snapshot(64)
|| [ nonce(12) || ct+tag per segment ]
= 168 octets, 224 Base64 chars
¶
This appendix is informative.¶
A two-segment passphrase-based encoding under the default SEAL-editable Profile, exercising the shared epoch key (at epoch length 16, segments 0 and 1 both derive epoch_key(0)), the segment AAD finality bit, and the two-contribution snapshot fold. All values are hex unless noted.¶
## Inputs
Passphrase: "correct horse battery staple" (28 octets)
Salt: 0101...0101 (32 octets of 0x01)
CEK: aaaaaaaa...aa (32 octets of 0xAA)
lock_nonce: 020202020202020202020202
payload_salt: 0404...0404 (32 octets of 0x04)
Segment 0: "Block zero data!" (16 octets)
Segment 1: "Final block." (12 octets, final)
seg0 nonce: 030303030303030303030303
seg1 nonce: 050505050505050505050505
## encryption_parameters (5 elements, KEK-bound)
encryption_parameters = [
aead_id: 0002 (AEAD_AES_256_GCM)
segment_size_be: 00010000 (= I2OSP(65536, 4))
kdf_id: 0001 (HKDF-SHA-256)
snap_id: 0001 (snapshot on)
epoch_length_u8: 10 (r = 16, SEAL-editable Profile)
]
payload_info additionally carries, before the salt:
nonce_mode: 00 (random)
## LOCK Key Derivation
step_binding:
00047061737300086172676f6e326964
00200101010101010101010101010101
01010101010101010101010101010101
0101
step_secret:
2bab4c606fb5c84123e48f0bb5eeb2a1
39a6dd996c6cff2efad2ad78a12143e0
derived_kek:
afafa8b7c85d5ffb1cb713c7844afed1
ad2821b25acabd26b96a724265c2083b
## Encrypted-CEK
Encrypted-CEK:
020202020202020202020202576e8f8a
46f8ab09684a3dfcd7ba493d328b875a
51b82e2f5bea35fe6e7582c5494e69f2
d64a154d317f6cb3f5448b9e
## Payload Schedule
payload_info = [aead_id, segment_size_be, kdf_id, snap_id,
nonce_mode, epoch_length_u8, salt] (7 elements)
payload_salt:
04040404040404040404040404040404
04040404040404040404040404040404
commitment:
c09e0f51871d431bc5624676212acf76
c8239ea5c8e54cd6b225d271122c4e8e
SealDerive("commit", CEK, [...payload_info, G], 32)
payload_key:
17f41951e5551ebf40eb781455df28f4
3ec6f249f1201892d3479f5e1ca500c4
SealDerive("payload_key", CEK, payload_info, 32)
snap_key:
b2aaae9c93648106d48c98063f310dc6
b227aef1aeee6e1a36515ea359dcd2e3
SealDerive("acc_key", CEK, payload_info, 32)
## Segment 0 (is_final=0)
nonce_0:
030303030303030303030303
data_aad_0:
00095345414c2d444154410008000000
0000000000000100
Encode("SEAL-DATA", uint64(0), uint8(0))
segment_key_0:
5d437730ddfc2e313028ea4ad3b346bf
fac22e0684a6721c388e6de66e9a1ca8
epoch_key(0) = SealDerive("epoch_key",
payload_key, [uint64(0)], 32)
ciphertext+tag_0:
6982dda75ef4e4df522bcc91869718a6
c5f91e976ca47a9b181e18ee7b5b9b6f
tag_0:
c5f91e976ca47a9b181e18ee7b5b9b6f
encrypted_block_0:
0303030303030303030303036982dda7
5ef4e4df522bcc91869718a6c5f91e97
6ca47a9b181e18ee7b5b9b6f
## Segment 1 (is_final=1)
nonce_1:
050505050505050505050505
data_aad_1:
00095345414c2d444154410008000000
0000000001000101
Encode("SEAL-DATA", uint64(1), uint8(1))
segment_key_1:
5d437730ddfc2e313028ea4ad3b346bf
fac22e0684a6721c388e6de66e9a1ca8
epoch_key(1 >> 16) = epoch_key(0),
the same key as segment 0
ciphertext+tag_1:
67ff80f4a0b9df1426d12535896e5208
0d8bbe80233148f1afb797fc
tag_1:
896e52080d8bbe80233148f1afb797fc
encrypted_block_1:
05050505050505050505050567ff80f4
a0b9df1426d12535896e52080d8bbe80
233148f1afb797fc
## Snapshot (snap_id 0x0001)
contrib_0:
4a0349a2a5b2487ec87b536a2149d0c9
19c9c8762cb664fdd242273a24223818
contrib_1:
df766e199abe6c7b8c6b3589b12cea43
07aa7ec91073c4c68c87862e9cac4c13
accumulator:
957527bb3f0c2405441066e390653a8a
1e63b6bf3cc5a03b5ec5a114b88e740b
= XOR of all contrib_i
snapshot_tag:
53972ea6b3021a4c5131bc9efe707e15
d1eeee0f336ec6c4909d27d1d304d87e
SnapTag: SealDerive("snapshot_tag", snap_key,
[uint64(2), accumulator], 32)
snapshot (Na=64) = wrapped_acc || snapshot_tag:
snapshot:
34f84e6b3b3debb10ea7c46cd21f4cc1
392d96050ad7733829b1bc909918edbf
53972ea6b3021a4c5131bc9efe707e15
d1eeee0f336ec6c4909d27d1d304d87e
wrapped_acc = accumulator XOR
snapmask(n_seg, snapshot_tag)
## Complete DATA object (212 octets)
DATA = salt(32) || commitment(32) || snapshot(64)
|| [ nonce(12) || ct+tag per segment ]
= 212 octets, 284 Base64 chars
¶
This appendix is informative. These vectors validate the SafeDerive function in isolation, independent of the SAFE protocol. Inputs are deliberately short for readability. See Section 5.4.1 for the definition.¶
Common inputs for all vectors:¶
label: "SAFE-TEST" (9 octets, hex: 534146452d54455354) ikm: 0a0b0c0d0e0f (6 octets) info: "" (0 octets)¶
Extract salt = "SAFE-v1":
534146452d7631
Extract ikm = Encode("SAFE-v1",
label, ...ikm):
0007534146452d76310009534146452d5445535400060a0b0c0d0e0f
prk = Extract(salt, ikm):
983d59830192955caf33fff4056ed415e2cd1cef7fe3072e075cf90903c97146
Expand info = Encode("SAFE-v1",
label, ...info,
I2OSP(32, 2)):
0007534146452d76310009534146452d54455354000000020020
output = Expand(prk, info, 32):
d7413c70bb7bde999f5e543c0796d63a0af6839ebbe5203cc526776b978ba147
¶
output = SafeDerive(label, ikm, info, 16): e190628e91995808047c49a7269b9d3b¶
The single-stage form frames ikm and info each as a single element (Section 4.3 of [I-D.sullivan-cfrg-raae]):¶
Derive(Encode("SAFE-v1", label,
Encode(...ikm),
Encode(...info),
I2OSP(32, 2)), 32):
input: 0007534146452d76310009534146452d54455354000800060a0b0c0d0e
0f000000020020
output: 03e6808974a8943c0055534d644ab7a6385e22cb45e68c7489ae35f7da
b55eff
¶
output = SafeDerive(label, ikm, info, 16): a25ddbeaa80e5196220df7ff048c24c9¶
These vectors exercise the combiner's handling of empty and multi-element ikm and info lists, and confirm the ikm/info partition is injective: splitting the elements as (A, B) and (C) versus (A) and (B, C) yields different outputs even though concatenating all elements gives the same octets. Each uses label "SAFE-TEST" with A = 11, B = 22, C = 33, and L = 32. Inputs are shown as ikm and info element lists.¶
ikm = [], info = [0a0b0c0d0e0f]
hkdf-sha-256: 6c08fb42193087c785c1b77679af1adc
ee37834ecb2ebd1ca0fb385ef85c7a48
turboshake256: efc1b6d7841cea2980ad2c03ce64773d
139cebb94cbff0c637eaa1ce9e870590
ikm = [0a0b, 0c0d0e0f], info = []
hkdf-sha-256: dfd4b424aa995f452a298be4de7b6c4b
1157e5729e63612f2c19de875ed1caa3
turboshake256: 1c46141340f6411c62059f64602bf086
908527ec272a26ff40ed71b0c3bec8eb
ikm = [0a0b0c0d0e0f], info = [1122, 3344]
hkdf-sha-256: 5d94a2cc1642a84fc19f03e556efc444
492d0102fab432bfa0d34baca0ec68c3
turboshake256: e5c3a82ede7d8bfda09bc790d8a927dd
aefae4f68823146d512a76afc086de7d
ikm = [A, B], info = [C] (boundary)
hkdf-sha-256: cf2c3e205da8633133566a28fb7291c4
d42ae36187c82351620d6263931f1b71
turboshake256: 742d5e666a872a2fdfa487a1c9d12767
2572612731745007c163acd85ed92e72
ikm = [A], info = [B, C] (boundary)
hkdf-sha-256: 6871955ff57be95706939812c6a4b607
698a319bc7ee6408d521cc30e42dc0a3
turboshake256: 22f8488d564a60a48e6290ddb68824e0
2618843998748d2ed233f40ad16fe583
¶
The two boundary outputs differ in both KDF classes: the two-stage KDF separates ikm and info across Extract and Expand, and the single-stage KDF frames ikm and info as distinct elements (Section 4.3 of [I-D.sullivan-cfrg-raae]).¶
This appendix is informative. These vectors demonstrate three properties of LOCK Key Derivation (Section 5.7.1): step order is bound, display-only descriptor fields are excluded from the binding, and every bound field changes the KEK. They exercise the KEK derivation directly and do not depend on how a step produces its secret, so the step secrets below are fixed illustrative values rather than the output of any particular step type.¶
Common inputs:¶
encryption_parameters (5 elements: AES-256-GCM, segment size 65536,
hkdf-sha-256, snapshot 0x0001, epoch 0x10):
aead_id 0002
segment_size_be 00010000
kdf_id 0001
snap_id 0001
epoch_length_u8 10
pass step bindings, Encode("pass", "argon2id", salt); the display-only
label is not a binding field:
bA (saltA = a1 repeated 16 times):
00047061737300086172676f6e3269640010
a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1
bB (saltB = b2 repeated 16 times):
00047061737300086172676f6e3269640010
b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2
step secrets (fixed illustrative values):
ssA = 51 repeated 32 times
ssB = 62 repeated 32 times
¶
The KEK is SafeDerive("lock-kek", step_secrets, [...encryption_parameters, step_binding_1, ..., step_binding_N], 32) per Section 5.7.1.¶
Reordering the steps changes the KEK. Both derivations use the same two (binding, secret) pairs in opposite order:¶
KEK([bA, bB], [ssA, ssB]) = d6d2eb11b2c9576b8fe62913764cde148cd31779dd085a371419af2d3424e441 KEK([bB, bA], [ssB, ssA]) = 30580bc1058200876045afe967e6344ce7e7bd235494cadc6cde8d48117996e4¶
The pass step binding is Encode("pass", kdf, salt); a descriptor's display-only label is not an input. Two descriptors that differ only in their label produce the identical binding bA and therefore the identical single-step KEK:¶
KEK([bA], [ssA]) = 67babd8d18ffac2c506b0804843cb8870db8ce3ba5a23ca24502f72dd7cdff40¶
Changing a bound field changes the KEK even when the step secret is unchanged. Both derivations use secret ssA; only the bound salt differs (bA carries saltA, bB carries saltB):¶
KEK([bA], [ssA]) = 67babd8d18ffac2c506b0804843cb8870db8ce3ba5a23ca24502f72dd7cdff40 KEK([bB], [ssA]) = c123cf374061810c7db73d58431146ffee6588f3b74391d55bd399097cbed498¶
This appendix is informative. It collects malformed inputs that a conforming Decryptor MUST reject, each with the normative requirement that mandates rejection. A conformance suite SHOULD include a negative case for each row.¶
| Malformed input | Rejection required by |
|---|---|
| Base64 with a character outside the standard alphabet, or padding in an invalid position | Section 4 |
| An integer field that is not canonical UINT (leading zero, sign, or whitespace) | Section 4 |
| A CONFIG block with a duplicate field name | Section 6.1 |
| A CONFIG field name not in the SAFE Config Options registry | Section 6.1 |
| A Profile value other than one of the six Section 5.8.1 registers | Section 6.1 |
| The SEAL-compact Profile combined with a non-MRAE AEAD | Section 6.1 |
| A CONFIG block containing a non-ASCII octet | Section 6.1 |
| A LOCK with multiple Encrypted-CEK lines or an unknown field name | Section 6.2.1 |
| A step descriptor with a parameter name not defined for its step type | Section 6.2 |
| A step descriptor whose parameters are out of the specified order | Section 6.2.1.1 |
| A step descriptor with a duplicate parameter name | Section 6.2.1.1 |
| A step descriptor missing a parameter its step type marks as required (pass without salt, hpke without kemct) | Section 6.2.1.1 |
| An Encrypted-CEK not decoding to Nn + 32 + 16 octets | Section 6.2 |
| A payload whose derived commitment does not match | Section 5.8.2 |
This appendix is informative.¶
New step types can be defined and registered to extend SAFE with additional authentication mechanisms. This section illustrates the process using three hypothetical steps: two Privacy Pass Key Derivation Function (PPKDF) steps for token-gated key derivation (Appendix I.1), and a WebAuthn PRF step for hardware token authentication.¶
Privacy Pass [RFC9578] type 0x0001 tokens use a Verifiable Oblivious Pseudorandom Function (VOPRF) [RFC9497]. The client constructs a TokenInput containing a nonce, blinds it, and sends the blinded element to the issuer. The issuer evaluates its PRF on the blinded element and returns the result. The client unblinds to obtain the authenticator, which is a deterministic function of the TokenInput and the issuer's secret key.¶
In normal issuance the nonce is random, making each token unique. PPKDF sets the nonce to a deterministic value derived from the SAFE context, so the authenticator is reproducible and serves as step key material. The issuer's behavior is unchanged and cannot distinguish a PPKDF request from normal token issuance.¶
Two step types use this mechanism:¶
ppkdf: token-gated key derivation with application-supplied context.¶
ppkdf-pass: token-gated key derivation with password-derived context for online throttling.¶
The ppkdf step provides token-gated key derivation with application-supplied context.¶
Token form:¶
ppkdf(issuer=tokens.example.com,salt=<Base64>)¶
Grammar:¶
ppkdf-step = "ppkdf(" ppkdf-params ")"
ppkdf-params = "issuer=" pp-host "," "salt=" salt
pp-host = host [ ":" port ]
host = 1*( ALPHA / DIGIT / "-" / "." )
port = 1*DIGIT
salt = 1*BASE64CHAR ; 44 chars = 32 octets
¶
Derivation:¶
Decode salt to salt_bytes (32 octets). Compute a deterministic nonce for the TokenInput:¶
nonce = SafeDerive("SAFE-PPKDF-NONCE",
"", [...encryption_parameters,
step_binding, salt_bytes], 32)
¶
where step_binding is the Encode-based binding form defined in Section 5.6. Construct a type 0x0001 TokenInput ([RFC9578], Section 5.1) with nonce as above. Set challenge_digest to:¶
SafeDerive("SAFE-PPKDF-CHALLENGE", TokenChallenge,
encryption_parameters, 32)
¶
using a TokenChallenge with issuer_name = issuer and empty redemption_context and origin_info.¶
Blind the TokenInput and send in a standard type 0x0001 TokenRequest. The issuer evaluates the VOPRF and returns a TokenResponse. Verify the VOPRF proof and unblind to obtain the authenticator (32 octets). Set step_secret = authenticator.¶
The ppkdf-pass step adds password-derived input. Each password guess requires a VOPRF evaluation from the issuer.¶
Token form:¶
ppkdf-pass(issuer=tokens.example.com,kdf=argon2id,salt=<Base64>)¶
Grammar:¶
ppkdf-pass-step = "ppkdf-pass(" ppkdf-pass-params ")"
ppkdf-pass-params = "issuer=" pp-host ","
"kdf=" kdf-name "," "salt=" salt
kdf-name = "argon2id" / "pbkdf2"
¶
The kdf parameter selects the password KDF using the same algorithms
and default parameters as the pass() step (Section 5.6.2).¶
Derivation:¶
Decode salt to salt_bytes (32 octets). Derive pw32 from the user's
password using the KDF indicated by the kdf parameter, with salt_bytes
as salt input and the default parameters defined in Section 5.6.2.
Compute the deterministic nonce:¶
nonce = SafeDerive("SAFE-PPKDF-PASS",
pw32, [...encryption_parameters,
step_binding], 32)
¶
Execute the PPKDF protocol as in the ppkdf step using this nonce. Set step_secret = authenticator.¶
Registration of ppkdf and ppkdf-pass is deferred to a separate document; this appendix is informative. A registration for these steps would include:¶
| Step Name | Parameters | Inputs | Secret | Ref |
|---|---|---|---|---|
| ppkdf | issuer=X, salt=X | PPKDF token | 32 octets | (this doc) |
| ppkdf-pass | issuer=X, kdf=X, salt=X | PPKDF token, password | 32 octets | (this doc) |
For ppkdf:¶
The issuer sees only the blinded_element. It cannot learn context, step_secret, or anything about the SAFE file. The VOPRF proof lets the client detect the wrong issuer key. Compromise of skI enables offline computation of step_secret for any context, breaking the online-gating property.¶
For ppkdf-pass:¶
Each password guess requires a VOPRF evaluation; issuer rate limits control guessing frequency. The issuer never sees the password-derived context. Compromise of skI still requires inverting the memory-hard KDF to recover the password.¶
Example LOCK block:¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: ppkdf(issuer=tokens.example.com,salt=<Base64>) Encrypted-CEK: Base64-encoded encrypted CEK -----END SAFE LOCK-----¶
A WebAuthn-based step would allow hardware token authentication using the PRF extension defined in Web Authentication Level 3 [W3C.webauthn-3]. Unlike WebAuthn assertions (signatures), the PRF extension provides deterministic output suitable for SAFE's step model.¶
webauthn-prf¶
The webauthn-prf step descriptor has three forms:¶
webauthn-prf(rpid=example.com,salt=xyz...) ; Identified RP webauthn-prf(salt=xyz...) ; Anonymous RP webauthn-prf(rpid=example.com,salt=xyz...,label=YubiKey) ; With label¶
The parameters are:¶
The WebAuthn relying party identifier. When present, the Decryptor uses this rpId for the WebAuthn ceremony. When omitted, selects anonymous RP mode.¶
The Base64-encoded PRF salt; must decode to exactly 32 octets. Generated at encryption time using SafeRandom.¶
A human-readable display name for this credential (e.g., "YubiKey", "Phone"). Not included in the step_binding; see Section 5.6.¶
Credential selection is delegated to the authenticator via WebAuthn's allowCredentials mechanism. The Decryptor passes all candidate credential IDs for the rpId; the authenticator selects the matching credential internally.¶
Grammar:¶
webauthn-prf-step = "webauthn-prf(" webauthn-params ")"
webauthn-params = [ "rpid=" rpid "," ] "salt=" salt
[ "," "label=" label-value ]
rpid = 1*( ALPHA / DIGIT / "-" / "." )
salt = 1*BASE64CHAR ; 44 chars = 32 octets
label-value = 1*( ALPHA / DIGIT / "-" )
¶
When rpid is omitted from the token, the Decryptor tries each rpId for which it holds credentials. Each rpId requires a separate WebAuthn ceremony (and potentially a user prompt). Privacy benefit: hides the relying party from passive observers. Cost: one ceremony per candidate rpId.¶
The authenticator evaluates the PRF extension with the selected credential and the decoded salt:¶
prf_salt = decode(salt) ; 32 octets
prf_output = WebAuthn_PRF(credential, prf_salt)
step_secret = SafeDerive(
"webauthn-prf", prf_output,
encryption_parameters, 32)
¶
Inputs: credential (local, selected by authenticator for the rpId), prf_salt (from token).¶
Encode("webauthn-prf", rpid, salt). rpid is UTF-8; salt is the raw decoded 32 octets. rpid is always present in the binding form even when omitted on-wire; when omitted, the Encryptor or Decryptor uses the rpId from the WebAuthn ceremony. Label is not included in binding.¶
salt must decode to exactly 32 octets. rpid, when present, must
match the hostname grammar 1*(ALPHA / DIGIT / "-" / ".").¶
Example LOCK:¶
-----BEGIN SAFE CONFIG----- Lock-Encoding: readable -----END SAFE CONFIG----- -----BEGIN SAFE LOCK----- Step: webauthn-prf(rpid=example.com,salt=xyz...) Encrypted-CEK: Base64-encoded encrypted CEK -----END SAFE LOCK-----¶
| Step Name | Parameters | Inputs | Secret | Ref |
|---|---|---|---|---|
| webauthn-prf | rpid=X, salt=X | Credential, rpId | 32 octets | (this doc) |
The WebAuthn PRF step provides hardware-bound key material (the authenticator holds the secret), user presence verification (touch required), phishing resistance (rpid binding), and offline decryption capability once the PRF output is computed.¶
The PRF extension requires WebAuthn Level 3 support in the browser. Non-discoverable credentials wrap key material in the credential ID; losing the credential ID means losing access to the encrypted file. Unlike the Privacy Pass steps, no server-side rate limiting is possible.¶
Privacy: rpid in cleartext reveals the relying party to passive observers. Anonymous RP mode (rpid omitted) hides this but the rpId may still be guessable from context.¶
Trial bounds: anonymous RP mode requires one WebAuthn ceremony per candidate rpId. Decryptors should impose a local bound on the number of rpIds to try. Prefer identified mode when privacy is not required.¶
Tightened the derived-mode segment index bound: in derived nonce mode, Encryptors MUST keep segment indices below 2^63 and Decryptors reject i >= 2^63 (2^64 in random mode), matching the packing of the segment index and finality bit into the derived nonce ([I-D.sullivan-cfrg-raae]). Indices at or above 2^63 cannot be represented in the derived nonce, so no valid object is affected.¶
Editorial clarifications, no wire change: step descriptor parameter requiredness is defined by the step's ABNF Parameters Grammar (Section 10.4); the Key Commitment section states that the commitment prefix covers the non-committing CEK-wrap AEAD, the [AGE-COMMIT] failure class; and the payload confidentiality claim is stated as ra-ROR with ra-CMT commitment ([FLOE]) rather than IND-CCA2.¶
Changed the Data-Encoding default from armored to binary, the one encoding defined over every layout, so an omitted field is valid for every Profile and the rule that SEAL-memory and SEAL-compact Encryptors set it explicitly is gone. Wire-visible: a Decryptor reads an object with no Data-Encoding field as binary, so armored objects state Data-Encoding: armored. Binary is now mandatory to implement and armored OPTIONAL. The example objects and known-answer files carry the explicit field; no derived value changes.¶
Renamed the Profile tokens to track raAE's renaming of the named instantiations for their use cases (Section 4.12 of [I-D.sullivan-cfrg-raae]): indexed became SEAL-compact, the write-once attachment became SEAL-simple, and the former simple became SEAL-editable. The letter names (SEAL-A through SEAL-E) are removed. SAFE names each Profile for the instantiation it adopts, so its tokens follow that document. No instantiation's parameters change, and the default Profile still selects the same instantiation, now written SEAL-editable. This is wire-visible. Every old token is absent from the new value list, so a Decryptor rejects such a CONFIG outright (Section 6.1) rather than resolving it to the wrong parameters.¶
Added negative and conformance test vectors (the LOCK Key Derivation Properties and Rejection Test Cases appendices) and a requirement that Decryptors MUST reject a step descriptor omitting a parameter its step type marks as required. It also notes that encryption_parameters' segment_size_be and payload_info's segment_max_be name the same Segment-Size value. No wire change, and existing vectors are unchanged.¶
Notation consistency (no wire change): the LOCK key derivation now writes its info argument as a single list, [...encryption_parameters, step_binding_1, ..., step_binding_N], rather than encryption_parameters || [step_binding_1, ...], so that || denotes octet-string concatenation only and list construction is written as a list. The Step Interface and LOCK derivation describe the ikm and info arguments to SafeDerive in KDF-neutral terms. Extract, Expand, and Derive no longer appear in the LOCK input formation, remaining in the KDF adapter definition (Section 5.4), the KDF-class description (Section 5.7.1), the security analysis, and the vectors. The Introduction states the access-path model (each LOCK is one independently satisfiable path to the CEK) consistent with Section 6.2. Derived bytes are unchanged.¶
Renamed "Block-Size" parameter to "Segment-Size" and "block" to "segment" throughout when referring to the encryption unit. "DATA block", "LOCK block", and "CONFIG block" (fence-marked section types) retain the "block" terminology.¶
Replaced "NMR" (Nonce-Misuse-Resistant) with "MRAE" (Misuse-Resistant Authenticated Encryption) per [RFC9771] terminology.¶
Added [RFC9771] reference for CMT-1/CMT-4 definitions and per-AEAD commitment status.¶
Added [I-D.irtf-cfrg-aead-limits] reference and integrity (forgery) bounds to Section 8.14.¶
Replaced readable strings in the key-schedule binding with encryption_parameters, a fixed five-element list of aead_id, segment_size_be, kdf_id, snap_id, and epoch_length_u8. aead_id, kdf_id, and snap_id are 2-octet big-endian numeric identifiers (I2OSP), replacing the earlier ASCII Binding Names; the Binding Name columns are now informative (Section 5.5). nonce_mode is not a CONFIG field or an encryption_parameters element; it is fixed by the Profile and bound only in payload_info. CONFIG tokens are unchanged; all derived keys, ciphertexts, and test vectors change.¶
Replaced the LOCK KEK aggregator chain (kek_init, kek_step, kek) with a single universal-combiner call, SafeDerive("lock-kek", the ordered step secrets, encryption_parameters followed by the ordered step bindings, Nk) (Section 5.7.1). A LOCK is the AND of its steps, bound in one derivation. KEK and Encrypted-CEK values change and the KEK test vectors are regenerated.¶
Renamed the CONFIG "Hash" field to "KDF" and its token sha-256 to hkdf-sha-256 (turboshake256 unchanged), since the field selects a key derivation function. The registry Binding Names ("HKDF-SHA-256", "TurboSHAKE-256") and all derived keys and test vectors are unchanged.¶
Reframed the LOCK key derivation presentation: renamed the "KEK Schedule" section "LOCK Key Derivation", stated the AND-within-a-LOCK and OR-between-LOCKs access policy explicitly, and added two informative diagrams (the multi-LOCK access policy and the swappable KDF). Informative only; no normative or wire change.¶
Aligned the payload epoch model with SEAL exactly: segment keys are always derived (segment_key(i) = epoch_key(i >> r), never the payload key directly). The epoch length is pinned by the selected SEAL named instantiation (Section 5.8.1), superseding an earlier per-AEAD default. Section 4.12 of [I-D.sullivan-cfrg-raae] resolves the epoch in every case, including for the 256-bit-nonce suites, which it treats independently of the instantiation. epoch_length_u8 is bound into both encryption_parameters and payload_info, so this changes KEK, Encrypted-CEK, commitment, payload_key, snap_key, and every payload-derived value; all affected vectors (Appendix G) are regenerated.¶
Added Binding Name columns to the SAFE AEAD Identifiers and SAFE KDF Identifiers registries.¶
Adopted the uniform SafeDerive input rule: in both KDF forms the encoded input ends with I2OSP(L, 2); the TurboSHAKE-256 single-stage form uses domain separator 0x1F.¶
Cross-references into [I-D.sullivan-cfrg-raae] now cite the current section number and title. The earlier numbers had drifted as that document was reorganized.¶
NORMATIVE: adopted the raAE named instantiations' pinned epoch lengths (Section 4.12 of [I-D.sullivan-cfrg-raae]) instead of the cipher suite's own default (Section 5.9.6), correcting a contradiction between SAFE's stated verbatim adoption of the SEAL named instantiations and its epoch_length_u8 wire value. SAFE now emits the epoch the instantiation resolves to, and states no epoch resolution rule of its own: Section 4.12 of [I-D.sullivan-cfrg-raae] resolves it in every case, including for the 256-bit-nonce suites, which it treats independently of the row. This changes encryption_parameters and payload_info, and therefore every vector that embeds epoch_length_u8 (Appendix G); all affected vectors and the machine-readable safe-v1-vectors.json are regenerated in this revision. Also renamed the reserved-label list entry "the segment nonce base" to "per-segment nonces in random mode": SAFE-NONCE labels the per-segment random nonce, and nonce_base is always SealDerive-derived, never SafeRandom-sampled.¶
Corrected the treatment of SEAL's global associated data G. The previous text pinned G as absent for every SAFE object and derived the commitment from the CEK and payload_info alone. raAE has no absent case: G is always the last element of the commitment info and defaults to the empty octet string, one zero-length element, so every commitment derivation includes it (Section 4.5.1 of [I-D.sullivan-cfrg-raae]). Because the KDF frames each element, omitting the zero-length element is a different input and yields a different commitment, so the old text did not interoperate with a conforming SEAL implementation. SAFE still supplies no global associated data; G is simply the empty octet string rather than absent. This changes every commitment value (Appendix G); the payload key, snap key, and nonce base never take G and are unchanged.¶
Realigned the aligned layout to the field order raAE fixes for it (Section 4.11.2 of [I-D.sullivan-cfrg-raae]). SAFE placed its own segment count N and first-ciphertext index D inside the raAE header and put the snapshot value after the per-segment metadata; raAE's order is salt, commitment, snapshot, the optional epoch-heads region, then the metadata, and Section 4.12 says the layout fixes field order and placement. N and D now sit in the prefix raAE reserves for a consuming protocol's own machinery, which is also what makes the epoch digest tree usable: its epoch-heads region is sized by the segment count, and raAE stores that count nowhere in the header. Wire-visible for the SEAL-memory and SEAL-compact Profiles, whose files change shape. An object written under the old order fails closed: a reader takes the old salt's first eight octets as N and D and reads the salt from the wrong offset, so payload_info carries a different salt and the stored commitment is read from the wrong place, and the commitment check (Section 8.13) rejects the object before any segment key is derived. The aligned read procedure (Section 6.4.2) now places that check immediately after the commitment is read, so it also precedes the reads sized by N, which nothing authenticates until SnapVerify. The header's total size is unchanged, so D and every segment offset are unchanged, and no vector is affected: none exercises the aligned layout.¶
Registered the two write-once aligned instantiations raAE added, SEAL-attachment (snap_id 0x0003, the epoch digest tree) and SEAL-attachment-small (snap_id 0x0002, the digest transcript), both SEAL-RO-v1 at segment size 65536 with a derived nonce and epoch length 10 (Section 4.12 of [I-D.sullivan-cfrg-raae]). SAFE now registers six of raAE's seven instantiations. They are for write-once content whose segments a reader verifies or decrypts individually. SAFE defines no new authenticator: it already defers construction to Section 4.7 of [I-D.sullivan-cfrg-raae]. Admitting them means the snapshot value length Na is no longer a constant, since 2 * Nh is the masked multiset hash's size alone, and each per-segment metadata entry is Nh octets longer under 0x0002 and 0x0003 because it stores that segment's leaf. It also means a snapshot no longer implies a mutable Profile: raAE's read-only profile admits 0x0000, 0x0002, or 0x0003, and the choice does not affect the write-once discipline. Wire-visible only in that two new Profile tokens are accepted; no existing object changes and no vector is affected.¶
Streaming Decryptors MUST NOT release decrypted plaintext before snapshot verification (SnapVerify) succeeds unless the weaker per-segment-only guarantee is documented (Section 5.8.7).¶
Stated the two truncation-detection mechanisms and the snapshot authenticator's lack of rollback freshness in Section 8.3. Applications requiring rollback protection MUST bind the snapshot value to external trusted state.¶
Added the offline passphrase-guessing oracle rationale to the passphrase guidance (Section 8.6); no conformance change.¶
Re-baselined the payload layer onto the SEAL named instantiations of [I-D.sullivan-cfrg-raae]. A single Profile CONFIG field names an instantiation by its usage nickname (Section 4.12 of [I-D.sullivan-cfrg-raae]), fixing the SEAL profile (SEAL-RW-v1 mutable or SEAL-RO-v1 immutable), the segment size, the nonce mode, the snapshot authenticator (snap_id), the epoch length, and the on-disk layout; the AEAD and KDF fields supply the cipher suite. payload_info becomes SEAL's seven-element form with uint16 aead_id/kdf_id code points, Nh is per-KDF (32 for HKDF-SHA-256, 64 for TurboSHAKE-256), and derived nonces bind is_final. The epoch length is pinned by the instantiation and is not configurable in CONFIG. The KEK schedule keeps SAFE-v1 and is unchanged. All payload-derived test vectors changed and are regenerated in place (Appendix G).¶
The snapshot authenticator's construction, naming, sizing, and operations are deferred to Section 4.7 of [I-D.sullivan-cfrg-raae]; SAFE keeps only the snap_id wire field (was agg_id), the opaque Na-octet snapshot value in the layout, the snap_key variable (was acc_key, KDF label string unchanged), and the SnapVerify-before-release gate. Byte-neutral; SAFE no longer names the construction.¶
Deferred more restated SEAL mechanics to [I-D.sullivan-cfrg-raae]: the epoch-key derivation formula (Section 4.5.2) and the derived-nonce safety conditions (Section 4.5.3.2), and dropped the duplicate commitment derivation formula (it remains in Section 5.8.2). SAFE keeps the SAFE-layer residue (the commitment-verify gate and the per-file salt nonce anchor). Also corrected the commitment collision bound to rest on collision resistance, not PRF security, per raAE Section 5.7.¶
Restricted armored Data-Encoding to the linear-layout Profiles: Decryptors MUST reject an armored object whose Profile fixes an aligned layout (Section 6.3) and MUST reject the SEAL-compact Profile with a non-MRAE AEAD (Section 6.1). SAFE does not register SEAL-disk: its split layout's two independently growing streams do not map onto the single-file SAFE format, and it may be added by a future revision. Also dropped the unused "payload" scope from the Config Options registry. Editorial: repaired the profile-keyed reading path, modernized stale random-nonce and encoding-keyed prose to the Profile model, and grouped the payload sections under a Payload Layer heading. The layout sections now defer stream structure to Section 4.11 of [I-D.sullivan-cfrg-raae] (the linear structure to 4.11.1 and 4.11.4, the metadata entry to 4.11.2); SAFE keeps its file carriage (prefix, headroom, offsets), its reader rules, and its worked minimums. Consolidated duplicated material: the per-instantiation guarantees matrix is now prose deferring to the Operational Summary (Section 5.1 of [I-D.sullivan-cfrg-raae]), duplicate and near-duplicate examples are merged, and repeated traffic-analysis, MRAE-re-mark, and plaintext-bound-nonce passages state each fact once.¶
Modernized the passages that predated the Profile model. The Profile fixes nonce_mode, snap_id, layout, and Data-Encoding together (Section 5.8.1), and several passages still read those as independent knobs. The MRAE suite was said to use derived nonces, though the Profile resolves the mode and AES-256-GCM-SIV is admitted under random-mode Profiles as well. Selective decryption branched on the encoding where the fields it reads belong to the layout, which left the default configuration with no offset formula and sent it to the aligned one. File extension read as a general procedure, though only a mutable Profile can be extended. The aligned segment-count limit told writers to use the linear layout, which the Profile fixes rather than the writer. "Snapshot on" and "Snapshot off" named a CONFIG field that does not exist, and the minimum linear payload was stated for two configurations no Profile produces. The security considerations called the commitment prefix the sole check binding snap_id, which encryption_parameters binds into the KEK. No wire format changes.¶