Refine encryption: FIDO2 preferred with passphrase fallback.

Automatic unlock resolution: try FIDO2 first (no typing, just touch),
fall back to passphrase if device not present. User never specifies
which method — sgard reads encryption.yaml and walks sources in order.

encrypt init --fido2 creates both sources (FIDO2 primary + passphrase
fallback) to prevent lockout on FIDO2 key loss. Separate salt files
per KEK source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 08:18:51 -07:00
parent 4d9e156eea
commit 079b235c9d

View File

@@ -322,17 +322,17 @@ requires re-wrapping the DEK, not re-encrypting every blob.
### KEK Derivation ### KEK Derivation
Two methods, selected at repo initialization: Two methods. A repo may have either or both:
**Passphrase:** **Passphrase:**
- KEK = Argon2id(passphrase, salt, time=3, memory=64MB, threads=4) - KEK = Argon2id(passphrase, salt, time=3, memory=64MB, threads=4)
- Salt stored at `<repo>/kek.salt` (16 random bytes) - Salt stored at `<repo>/kek-passphrase.salt` (16 random bytes)
- Argon2id parameters stored alongside the salt for forward compatibility - Argon2id parameters stored in `encryption.yaml` for forward compat
**FIDO2 hmac-secret:** **FIDO2 hmac-secret:**
- KEK = HMAC-SHA256 output from the FIDO2 authenticator - KEK = HMAC-SHA256 output from the FIDO2 authenticator
- The authenticator computes `HMAC(device_secret, salt)` where the salt - The authenticator computes `HMAC(device_secret, salt)` where the salt
is stored at `<repo>/kek.salt` is stored at `<repo>/kek-fido2.salt`
- Requires a FIDO2 key that supports the `hmac-secret` extension - Requires a FIDO2 key that supports the `hmac-secret` extension
- User touch is required to derive the KEK - User touch is required to derive the KEK
@@ -413,18 +413,39 @@ at `<repo>/dek.enc`:
[24-byte nonce][encrypted DEK + 16-byte tag] [24-byte nonce][encrypted DEK + 16-byte tag]
``` ```
### Multiple KEK Sources ### KEK Sources and Unlock Resolution
A repo can have multiple KEK sources (e.g., both a passphrase and a A repo can have one or both KEK sources. Each wraps the same DEK
FIDO2 key). Each source wraps the same DEK independently: independently:
``` ```
<repo>/dek.enc.passphrase # DEK wrapped by passphrase-derived KEK <repo>/dek.enc.passphrase # DEK wrapped by passphrase-derived KEK
<repo>/dek.enc.fido2 # DEK wrapped by FIDO2-derived KEK <repo>/dek.enc.fido2 # DEK wrapped by FIDO2-derived KEK
``` ```
Either source can unwrap the DEK. Adding a new source requires the DEK Either source can unwrap the DEK. Adding a second source requires the
(unlocked by any existing source) to create the new wrapped copy. DEK (unlocked by the existing source) to create the new wrapped copy.
**Automatic unlock resolution (no user flags needed):**
When sgard needs the DEK, it reads `encryption.yaml` to discover which
sources are configured, then tries them in order:
1. **FIDO2** (if `dek.enc.fido2` exists):
- Check if a FIDO2 device is connected
- If yes → prompt for touch, derive KEK, unwrap DEK
- If device not found or touch times out → fall through
2. **Passphrase** (if `dek.enc.passphrase` exists):
- Prompt for passphrase on stdin
- Derive KEK via Argon2id, unwrap DEK
3. **No sources succeed** → error
FIDO2 is preferred because it requires no typing — just a touch. The
passphrase is the fallback for when the FIDO2 key isn't physically
present (e.g., on a different machine). The user never specifies which
method to use; sgard figures it out.
### Repo Configuration ### Repo Configuration
@@ -433,29 +454,36 @@ Encryption config stored at `<repo>/encryption.yaml`:
```yaml ```yaml
algorithm: xchacha20-poly1305 algorithm: xchacha20-poly1305
kek_sources: kek_sources:
- type: fido2
salt_file: kek-fido2.salt
dek_file: dek.enc.fido2
- type: passphrase - type: passphrase
argon2_time: 3 argon2_time: 3
argon2_memory: 65536 # KiB argon2_memory: 65536 # KiB
argon2_threads: 4 argon2_threads: 4
salt_file: kek.salt salt_file: kek-passphrase.salt
dek_file: dek.enc.passphrase dek_file: dek.enc.passphrase
- type: fido2
salt_file: kek.salt
dek_file: dek.enc.fido2
``` ```
The presence of `encryption.yaml` indicates the repo has a DEK The `kek_sources` list is ordered by preference (FIDO2 first). The
(encryption capability). Individual files opt in via `--encrypt` at presence of `encryption.yaml` indicates the repo has encryption
add time. capability. Individual files opt in via `--encrypt` at add time.
### CLI Integration ### CLI Integration
**Setting up encryption (creates DEK and first KEK source):** **Setting up encryption (creates DEK and wraps it):**
```sh ```sh
sgard encrypt init # prompts for passphrase sgard encrypt init # passphrase only
sgard encrypt init --fido2 # uses FIDO2 key sgard encrypt init --fido2 # FIDO2 + passphrase fallback
``` ```
When `--fido2` is specified, sgard creates both sources: the FIDO2
wrap (primary) and immediately prompts for a passphrase to create the
fallback wrap. This ensures the user is never locked out if they lose
the FIDO2 key.
Without `--fido2`, only the passphrase source is created.
**Adding encrypted files:** **Adding encrypted files:**
```sh ```sh
sgard add --encrypt ~/.ssh/config ~/.aws/credentials sgard add --encrypt ~/.ssh/config ~/.aws/credentials
@@ -464,8 +492,8 @@ sgard add ~/.bashrc # not encrypted
**Adding a KEK source to an existing repo:** **Adding a KEK source to an existing repo:**
```sh ```sh
sgard encrypt add-passphrase # add passphrase (requires existing unlock) sgard encrypt add-fido2 # add FIDO2 (auto-unlocks via passphrase first)
sgard encrypt add-fido2 # add FIDO2 key (requires existing unlock) sgard encrypt add-passphrase # add passphrase (auto-unlocks via FIDO2 first)
``` ```
**Changing a passphrase:** **Changing a passphrase:**
@@ -475,8 +503,8 @@ sgard encrypt change-passphrase # prompts for old and new
**Unlocking:** **Unlocking:**
Operations that touch encrypted entries (add --encrypt, checkpoint, Operations that touch encrypted entries (add --encrypt, checkpoint,
restore, diff, mirror on encrypted files) prompt for the passphrase restore, diff, mirror on encrypted files) trigger automatic unlock
or FIDO2 touch automatically. The DEK is cached in memory for the via the resolution order above. The DEK is cached in memory for the
duration of the command. duration of the command.
Operations that only touch plaintext entries never prompt — they work Operations that only touch plaintext entries never prompt — they work