Update encryption design: selective per-file encryption, punt signing.

Encryption is per-file (--encrypt flag on add), not per-repo. A repo
can have a mix of encrypted and plaintext blobs. Commands that only
touch plaintext entries never prompt for the DEK.

Manifest signing deferred — the trust model (which key signs, how do
pulling clients verify across multiple machines) needs proper design.

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

View File

@@ -286,10 +286,10 @@ SSH key resolution order (for initial authentication):
## Encryption ## Encryption
sgard supports optional at-rest encryption for blob contents. When sgard supports optional at-rest encryption for individual files.
enabled, files are encrypted before being stored in the blob store and Encryption is per-file, not per-repo — any file can be marked as
decrypted on restore. Encryption is per-repo — a repo is either encrypted at add time. A repo may contain a mix of encrypted and
encrypted or it isn't. plaintext blobs.
### Key Hierarchy ### Key Hierarchy
@@ -376,23 +376,33 @@ Rationale:
**Manifest changes for encryption:** **Manifest changes for encryption:**
To support `status` without decrypting every blob, the manifest entry Encrypted entries gain two fields: `encrypted: true` and
gains an optional `plaintext_hash` field: `plaintext_hash` (SHA-256 of the plaintext, for efficient `status`
checks without decryption):
```yaml ```yaml
files: files:
- path: ~/.bashrc - path: ~/.bashrc
hash: a1b2c3d4... # SHA-256 of encrypted blob (post-encryption) hash: a1b2c3d4... # SHA-256 of plaintext — not encrypted
plaintext_hash: e5f6a7... # SHA-256 of plaintext (pre-encryption)
type: file type: file
mode: "0644" mode: "0644"
updated: "2026-03-24T..." updated: "2026-03-24T..."
- path: ~/.ssh/config
hash: f8e9d0c1... # SHA-256 of encrypted blob (post-encryption)
plaintext_hash: e5f6a7... # SHA-256 of plaintext (pre-encryption)
encrypted: true
type: file
mode: "0600"
updated: "2026-03-24T..."
``` ```
For unencrypted entries, `hash` is the SHA-256 of the plaintext (current
behavior), and `plaintext_hash` and `encrypted` are omitted.
`status` hashes the current file on disk and compares against `status` hashes the current file on disk and compares against
`plaintext_hash`. This avoids decrypting stored blobs just to check `plaintext_hash` (for encrypted entries) or `hash` (for plaintext).
if a file has changed. `verify` uses `hash` (the encrypted blob hash) `verify` always uses `hash` to check store integrity without the DEK.
to check store integrity without the DEK.
### DEK Storage ### DEK Storage
@@ -421,7 +431,6 @@ Either source can unwrap the DEK. Adding a new source requires the DEK
Encryption config stored at `<repo>/encryption.yaml`: Encryption config stored at `<repo>/encryption.yaml`:
```yaml ```yaml
enabled: true
algorithm: xchacha20-poly1305 algorithm: xchacha20-poly1305
kek_sources: kek_sources:
- type: passphrase - type: passphrase
@@ -435,15 +444,25 @@ kek_sources:
dek_file: dek.enc.fido2 dek_file: dek.enc.fido2
``` ```
The presence of `encryption.yaml` indicates the repo has a DEK
(encryption capability). Individual files opt in via `--encrypt` at
add time.
### CLI Integration ### CLI Integration
**Enabling encryption:** **Setting up encryption (creates DEK and first KEK source):**
```sh ```sh
sgard init --encrypt # prompts for passphrase sgard encrypt init # prompts for passphrase
sgard init --encrypt --fido2 # uses FIDO2 key sgard encrypt init --fido2 # uses FIDO2 key
``` ```
**Adding a KEK source to an existing encrypted repo:** **Adding encrypted files:**
```sh
sgard add --encrypt ~/.ssh/config ~/.aws/credentials
sgard add ~/.bashrc # not encrypted
```
**Adding a KEK source to an existing repo:**
```sh ```sh
sgard encrypt add-passphrase # add passphrase (requires existing unlock) sgard encrypt add-passphrase # add passphrase (requires existing unlock)
sgard encrypt add-fido2 # add FIDO2 key (requires existing unlock) sgard encrypt add-fido2 # add FIDO2 key (requires existing unlock)
@@ -455,9 +474,13 @@ sgard encrypt change-passphrase # prompts for old and new
``` ```
**Unlocking:** **Unlocking:**
Operations that need the DEK (add, checkpoint, restore, diff, mirror) Operations that touch encrypted entries (add --encrypt, checkpoint,
prompt for the passphrase or FIDO2 touch automatically. The unlocked restore, diff, mirror on encrypted files) prompt for the passphrase
DEK can be cached in memory for the duration of the command. or FIDO2 touch automatically. The DEK is cached in memory for the
duration of the command.
Operations that only touch plaintext entries never prompt — they work
exactly as before, even if the repo has encryption configured.
There is no long-lived unlock state — each command invocation that needs There is no long-lived unlock state — each command invocation that needs
the DEK obtains it fresh. This is intentional: dotfile operations are the DEK obtains it fresh. This is intentional: dotfile operations are
@@ -466,24 +489,35 @@ daemon or on-disk secret, both of which expand the attack surface.
### Security Properties ### Security Properties
- **At-rest confidentiality:** Blobs are encrypted. The manifest - **Selective confidentiality:** Only files marked `--encrypt` are
contains paths and hashes but not file contents. encrypted. The manifest contains paths and hashes but not file
contents for encrypted entries.
- **Server ignorance:** The server never has the DEK. Push/pull - **Server ignorance:** The server never has the DEK. Push/pull
transfers encrypted blobs. The server cannot read file contents. transfers encrypted blobs opaquely. The server cannot read encrypted
file contents.
- **Key rotation:** Changing the passphrase re-wraps the DEK without - **Key rotation:** Changing the passphrase re-wraps the DEK without
re-encrypting blobs. re-encrypting blobs.
- **Compromise recovery:** If the DEK is compromised, all blobs must - **Compromise recovery:** If the DEK is compromised, all encrypted
be re-encrypted (not just re-wrapped). This is an explicit `sgard blobs must be re-encrypted (not just re-wrapped). This is an
encrypt rotate-dek` operation. explicit `sgard encrypt rotate-dek` operation.
- **No plaintext leaks:** `diff` decrypts in memory, never writes - **No plaintext leaks:** `diff` decrypts in memory, never writes
plaintext blobs to disk. decrypted blobs to disk.
- **Graceful degradation:** Commands that don't touch encrypted entries
work without the DEK. A `status` on a mixed repo can check plaintext
entries without prompting.
### Non-Encrypted Repos ### Repos Without Encryption
Encryption is optional. Repos without encryption work exactly as before A repo with no `encryption.yaml` has no DEK and cannot have encrypted
— no `encryption.yaml`, no DEK, blobs stored as plaintext. The `hash` entries. The `--encrypt` flag on `add` will error, prompting the user
field in the manifest is the SHA-256 of the plaintext (same as current to run `sgard encrypt init` first. All existing behavior is unchanged.
behavior). The `plaintext_hash` field is omitted.
### Future: Manifest Signing
Manifest signing (to detect tampering) is deferred. The challenge is
the trust model: which key signs, and how does a pulling client verify
the signature when multiple machines with different SSH keys push to
the same server? This requires a proper trust/key-authority design.
## Go Package Structure ## Go Package Structure