Password Backend
This page describes the password authentication backend implemented in
core-auth/src/password.rs and core-auth/src/password_wrap.rs. The backend uses Argon2id to
derive a key-encrypting key (KEK) from user-provided password bytes, then wraps or unwraps a
32-byte master key using AES-256-GCM.
PasswordBackend
The PasswordBackend struct holds an optional SecureVec containing password bytes. Password
bytes must be injected via with_password() (builder pattern) or set_password() (mutation)
before calling unlock() or enroll(). The SecureVec type provides mlock’d memory and
zeroize-on-drop semantics.
Trait Implementation
| Method | Behavior |
|---|---|
factor_id() | Returns AuthFactorId::Password |
name() | Returns "Password" |
backend_id() | Returns "password" |
is_enrolled(profile, config_dir) | Checks whether {config_dir}/vaults/{profile}.password-wrap exists |
can_unlock(profile, config_dir) | Returns true only if enrolled AND password bytes have been set |
requires_interaction() | Returns AuthInteraction::PasswordEntry |
KEK Derivation
The derive_kek() method performs:
- Validate salt is exactly 16 bytes.
- Call
core_crypto::derive_key_argon2(password, salt)– Argon2id with project-wide parameters. - Copy the first 32 bytes of the Argon2id output into a
[u8; 32]KEK array.
The Argon2id parameters are defined in core-crypto (not in core-auth).
Unlock Flow
- Read password bytes from the stored
SecureVec. Fail withBackendNotApplicableif no password was set. - Load the
PasswordWrapBlobfrom{config_dir}/vaults/{profile}.password-wrap. - Derive the KEK via
derive_kek(password, salt). - Call
blob.unwrap(&mut kek)to decrypt the master key via AES-256-GCM. The KEK is zeroized after use. - Return an
UnlockOutcomewithipc_strategy: DirectMasterKeyandfactor_id: Password.
Enrollment Flow
- Read password bytes from the stored
SecureVec. - Derive the KEK via
derive_kek(password, salt). - Call
PasswordWrapBlob::wrap(master_key, &mut kek)to encrypt the master key. The KEK is zeroized after use. - Write the blob to disk via
blob.save(config_dir, profile).
Revocation
Revocation overwrites the wrap file with zeros before deletion to prevent casual recovery from disk:
- Read the file length.
- Write a zero-filled buffer of the same length.
- Delete the file via
std::fs::remove_file.
PasswordWrapBlob
Defined in core-auth/src/password_wrap.rs, the PasswordWrapBlob struct represents the
on-disk binary format for the AES-256-GCM wrapped master key.
Binary Format
Offset Length Field
0 1 Version byte (0x01)
1 12 Nonce (random, generated via getrandom)
13 48 Ciphertext (32-byte master key + 16-byte GCM tag)
Total size: 61 bytes.
The version constant PASSWORD_WRAP_VERSION is 0x01.
Wrapping (Encryption)
PasswordWrapBlob::wrap(master_key, kek_bytes):
- Construct an
EncryptionKeyfrom the 32-byte KEK. - Generate a 12-byte random nonce via
getrandom. - Encrypt the master key with AES-256-GCM using the KEK and nonce.
- Zeroize the KEK bytes.
- Return the blob containing version, nonce, and ciphertext.
Unwrapping (Decryption)
PasswordWrapBlob::unwrap(kek_bytes):
- Construct an
EncryptionKeyfrom the 32-byte KEK. - Zeroize the KEK bytes immediately after key construction.
- Decrypt using AES-256-GCM with the stored nonce and ciphertext.
- Return the plaintext as
SecureBytes(mlock’d, zeroize-on-drop). - If GCM authentication fails (wrong password), return
AuthError::UnwrapFailed.
Deserialization
PasswordWrapBlob::deserialize(data) rejects:
- Data shorter than 61 bytes (
AuthError::InvalidBlob). - Version bytes other than
0x01(AuthError::InvalidBlob).
Persistence
Path: {config_dir}/vaults/{profile}.password-wrap
Write: save() uses atomic rename via a .password-wrap.tmp intermediate file. On Unix,
file permissions are set to 0o600 (owner read/write only) before the rename. The parent
vaults/ directory is created if it does not exist.
Read: load() reads the file and calls deserialize().
Zeroization
The PasswordWrapBlob struct implements Drop to zeroize its nonce and ciphertext
fields. All KEK arrays are zeroized immediately after use in both wrap() and unwrap().
Salt
Each profile has an independent 16-byte salt stored at {config_dir}/vaults/{profile}.salt.
The salt is generated via getrandom during vault initialization
(daemon-secrets/src/unlock.rs::generate_profile_salt).
During sesame init, the salt file is written with:
- The
vaults/directory created with permissions0o700. - The salt file itself written via
core_config::atomic_writeand then set to permissions0o600.
The salt is used as input to both the Argon2id KDF (password backend) and the BLAKE3 challenge derivation (SSH agent backend). Using a per-profile salt ensures that the same password produces different KEKs for different profiles.
Key Material Handling
The password backend’s key material lifecycle:
-
Password bytes: Stored in
SecureVec(mlock’d, zeroize-on-drop). Acquired from the user viadialoguer::Password(terminal) or stdin (pipe). TheStringholding the raw password is zeroized immediately after copying into theSecureVec. -
KEK (Argon2id output): A
[u8; 32]stack array. Zeroized byPasswordWrapBlob::wrap()andPasswordWrapBlob::unwrap()after use. -
Master key: Returned as
SecureBytes(backed byProtectedAlloc– mlock’d, mprotect’d, zeroize-on-drop). Transferred to daemon-secrets viaSensitiveBytesIPC wrapper which also usesProtectedAlloc.
At no point does the master key exist in an unprotected heap allocation. The KEK exists briefly on the stack and is zeroized before the function returns.