Files
arca/PLAN.md
Kyle Isom c835358829 Initial implementation of arca, a LUKS volume manager.
Go CLI using cobra with mount, unmount, status, and init subcommands.
Unlocks via udisks2 D-Bus (passphrase/keyfile) or cryptsetup (FIDO2/TPM2)
with ordered method fallback. Includes NixOS-specific LD_LIBRARY_PATH
injection for systemd cryptsetup token plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 07:42:38 -07:00

169 lines
5.6 KiB
Markdown

# arca — Implementation Plan
## Overview
A Go CLI that manages LUKS volumes. Uses udisks2 D-Bus for
passphrase/keyfile unlock (no root needed), and falls back to
`cryptsetup` for FIDO2/TPM2 tokens (requires root/doas). Three
subcommands: `mount`, `unmount`, `status`.
## Dependencies
- `github.com/spf13/cobra` — CLI framework
- `github.com/godbus/dbus/v5` — D-Bus client
- `gopkg.in/yaml.v3` — config file parsing
## Project Structure
```
arca/
├── main.go
├── cmd/
│ ├── root.go # root command, global flags
│ ├── mount.go # mount subcommand
│ ├── unmount.go # unmount subcommand
│ └── status.go # status subcommand
├── internal/
│ ├── udisks/ # udisks2 D-Bus client
│ │ ├── client.go # connection + device discovery
│ │ ├── encrypt.go # Unlock / Lock operations
│ │ ├── mount.go # Mount / Unmount operations
│ │ └── types.go # object path helpers, constants
│ ├── cryptsetup/ # cryptsetup CLI wrapper (FIDO2/TPM2)
│ │ └── cryptsetup.go
│ ├── unlock/ # method sequencing logic
│ │ └── unlock.go
│ └── config/
│ └── config.go # YAML config loading + device resolution
├── flake.nix
├── go.mod
├── go.sum
├── README.md
└── PLAN.md
```
## Phase 1: Core Unlock/Lock Backend
Build `internal/udisks/` for D-Bus operations and `internal/cryptsetup/`
for FIDO2/TPM2 fallback.
### udisks2 D-Bus (passphrase + keyfile)
1. Connect to the system bus.
2. Enumerate block devices under `/org/freedesktop/UDisks2/block_devices/`.
3. Find a block device by device path or UUID.
4. `Unlock(passphrase)` — call `org.freedesktop.UDisks2.Encrypted.Unlock`
on the LUKS partition. Returns the cleartext device object path.
5. `Mount()` — call `org.freedesktop.UDisks2.Filesystem.Mount` on the
cleartext device. Returns the mount point path.
6. `Unmount()` + `Lock()` — reverse operations.
Key D-Bus details:
- Bus name: `org.freedesktop.UDisks2`
- Object paths: `/org/freedesktop/UDisks2/block_devices/<name>`
- Interfaces: `org.freedesktop.UDisks2.Encrypted` (Unlock, Lock),
`org.freedesktop.UDisks2.Filesystem` (Mount, Unmount),
`org.freedesktop.UDisks2.Block` (device properties, UUID lookup)
### cryptsetup fallback (FIDO2 + TPM2)
udisks2 does not support FIDO2/TPM2 token-based keyslots. For these,
fall back to invoking `cryptsetup` (via doas/sudo):
- FIDO2: `cryptsetup open --token-type systemd-fido2 <device> <name>`
- TPM2: `cryptsetup open --token-type systemd-tpm2 <device> <name>`
- `--token-only` can attempt all enrolled tokens automatically.
After `cryptsetup open`, mount via udisks2 D-Bus or plain `mount`.
### Unlock method sequencing
Each device has an ordered list of methods to try. The unlock logic walks
the list in order, stopping at the first success:
1. Try the first method (e.g., `fido2`).
2. If it fails (device not plugged in, timeout, etc.), try the next.
3. Continue until one succeeds or all are exhausted.
Default when no config exists: `[passphrase]`.
## Phase 2: Config File
Build `internal/config/`:
1. Load `~/.config/arca/config.yaml` (respect `$XDG_CONFIG_HOME`).
2. Resolve an alias to a UUID and unlock methods.
3. If no config file exists, that's fine — device path arguments still work
with the default method sequence `[passphrase]`.
Config schema:
```yaml
devices:
backup:
uuid: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
mountpoint: "/mnt/backup" # optional
methods: # optional, default: [passphrase]
- fido2
- passphrase
media:
uuid: "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
methods:
- keyfile
- passphrase
keyfile: "/path/to/media.key" # required if keyfile is in methods
```
Supported methods: `passphrase`, `keyfile`, `fido2`, `tpm2`.
## Phase 3: CLI Commands
Wire up cobra subcommands:
### `arca mount <device|alias>`
1. Resolve argument: if it matches a config alias, look up UUID and
methods list; otherwise treat as a device path with default methods.
2. Find the block device via udisks2.
3. Walk the methods list in order, attempting each unlock strategy:
- `passphrase` / `keyfile`: via udisks2 D-Bus (no root).
- `fido2` / `tpm2`: via `cryptsetup` CLI (requires doas/sudo).
- Stop at the first success; if all fail, report each error.
4. Mount the resulting cleartext device via udisks2.
5. Print the mount point.
### `arca unmount <device|alias>`
1. Resolve argument to the LUKS block device.
2. Find the associated cleartext (mapped) device.
3. Call Unmount on the cleartext device.
4. Call Lock on the LUKS device.
### `arca status`
1. Enumerate all block devices with the Encrypted interface.
2. For each, check if unlocked (has a cleartext device).
3. If mounted, show mount point.
4. Print a table: device, UUID, alias (if configured), state, mount point.
## Phase 4: Credential Input
- **Passphrase**: read from terminal with echo disabled (`golang.org/x/term`).
Alternatively, accept from stdin for scripting.
- **Keyfile**: read path from config, pass contents to udisks2.
- **FIDO2**: `cryptsetup` handles the token interaction (tap prompt comes
from libfido2).
- **TPM2**: `cryptsetup` handles TPM unsealing automatically.
## Phase 5: Packaging
- `flake.nix` with `buildGoModule`.
- Add as a flake input to the NixOS config repo.
## Open Questions
- Should `mount` accept a `--mountpoint` flag to override the default?
- Should there be a `--dry-run` flag that shows what D-Bus calls would be
made?
- Is there value in a `--json` output flag for `status`?