# Encrypted vault format The application stores the vault as UTF-8 TOML, schema version 1. The bundle is an opaque encrypted unit suitable for persistence, backup, and restoration without the vault password. Parsing is bounded and rejects unsupported versions, unknown fields, malformed labels, invalid Base64, and oversized bundles. ## Version 1 structure For M4, the current encrypted bundle is intended to live at `ResolvedSettings::data_directory.join("vault.toml")`. This stable filename is relative to the resolved data directory, whose default is `Backup/data` beside the executable. M3 only prepares and serializes vault bytes in memory; it does not create this file or install application state. M4 persistence will own that write. The top level contains: | Field | Content | | --- | --- | | `version` | Integer `1`. Other values are rejected. | | `recipient` | Canonical age x25519 public recipient (`age1...`). | | `identity_ciphertext` | Standard Base64 of an age binary file encrypted with the passphrase recipient format (scrypt). Its plaintext is the age x25519 secret identity. | | `records` | TOML table keyed by each client's canonical WireGuard public key. | Each record table contains `ciphertext`, standard Base64 of an age binary file encrypted to `recipient`, and `reissue_blocked`, an advisory status snapshot. The snapshot is not authorization. Retrieval decrypts the record and enforces its authenticated `reissue_blocked` value; editing the TOML boolean cannot authorize a blocked key. Record insertion rejects an existing public-key label and never silently replaces ciphertext. The TOML shape is: ```toml version = 1 recipient = "age1..." identity_ciphertext = "" [records.""] ciphertext = "" reissue_blocked = false ``` The plaintext of a decrypted client record is UTF-8 TOML with this schema: ```text version = 1 label = "" private_key = "" client_addresses = [""] interface_dns = "" interface_mtu = "" server_public_key = "" endpoint = "" allowed_ips = [""] persistent_keepalive = "" preshared_key = "" reissue_blocked = true ``` Optional fields are omitted when absent. Unknown input directives such as hooks are not interpreted or executed, and are not part of the retained delivery parameters. The record label is authenticated by age encryption and repeated inside the ciphertext. On retrieval the implementation checks the embedded label, derives the public key from the decrypted private key, and enforces the encrypted conflict bit. Swapping a ciphertext to another public-key table entry therefore fails closed. No alias, filename, or server peer key is used as client identity. Client imports derive identity only from `[Interface] PrivateKey`. The `[Peer] PublicKey` is retained as the server identity. Address discrepancies, a confirmed server-key mismatch, an unresolved authoritative server identity, file-only clients, unsupported address projection, invalid inputs, and duplicate candidates are separate report outcomes. An unresolved identity is not treated as a match. A valid key may be encrypted while a conflict is present, but password-authorized retrieval remains blocked. Clearing that authenticated state is deferred until an explicit reconciliation operation can verify both client material and a fresh authoritative server configuration; the current API intentionally provides no public-boolean or metadata-only bypass. ## Read-only source scan The configured client source directory is scanned non-recursively. Immediate regular files ending in `.conf` (ASCII case-insensitive) are candidates; other names and nested directories are ignored. Symlinks and non-regular files are not read. Each candidate is capped at 1 MiB, a scan at 4,096 directory entries, 1,024 candidates, and 32 MiB of input. File/open/read/size errors are reported by safe category and do not stop other candidates. A directory or enumeration failure and any per-file read/limit failure mark the scan incomplete. Initial setup returns the encrypted in-memory bundle and report; installing it and committing remain separate operations. Explicit later scans encrypt to the existing public recipient and preserve the protected identity. Source files are never modified. If a later import finds an existing public-key record, the prior ciphertext is preserved and the report identifies that duplicate; the scanner cannot establish plaintext equality without decrypting the prior record, so it does not claim the files are equal. ## Limits and failure behavior - Serialized TOML bundle: 16 MiB maximum. - Records: 4,096 maximum. - One encrypted record: 64 KiB maximum, before Base64 encoding. - Password-encrypted identity: 16 KiB maximum, before Base64 encoding. - The serialized representation must fit within the bundle limit. Malformed/truncated ciphertext, a recipient that differs from the protected identity, unknown versions, missing records, and relabelled records return secret-free typed errors. An unsupported decrypted record schema returns the version error, while a payload whose label does not match the requested public key returns the record mismatch error. There is no fallback to plaintext files. The bundle loader does not unlock the identity, so backup and restore require no password. ## Secret lifetime during encoding New-client encryption needs only the public age recipient. Retrieval constructs an age scrypt identity for one call, decrypts the protected x25519 identity, verifies it against the public recipient, decrypts one record, and drops the temporary identity and plaintext on return. The application API does not expose an unlocked vault identity or retain a password between calls. The WireGuard key's explicit Base64 representation and the record plaintext containing it must exist briefly in process memory before age can encrypt them. Age also creates and buffers its file key, stream state, and output ciphertext. These copies are inherent to the selected age streaming API; no plaintext is written into the TOML bundle. The in-memory private-key type, passphrase type, and temporary plaintext buffers use zeroizing secret containers where supported. Age's standard passphrase format uses its normal scrypt work factor; the application does not override or weaken it. The caller remains responsible for keeping the input password string short-lived. The password protects only the vault identity. It does not encrypt server configuration, retained original client files, the entire backup archive, or WireGuard server secrets stored elsewhere.