Step 18: FIDO2 support with interface and mock.
FIDO2Device interface abstracts hardware interaction (Register, Derive, Available, MatchesCredential). Real libfido2 implementation deferred; mock device used for full test coverage. AddFIDO2Slot: registers FIDO2 credential, derives KEK via HMAC-secret, wraps DEK, adds fido2/<label> slot to manifest. UnlockDEK: tries all fido2/* slots first (checks credential_id against connected device), falls back to passphrase. User never specifies which method. 6 tests: add slot, reject duplicate, unlock via FIDO2, fallback to passphrase when device unavailable, slot persistence, encrypted round-trip unlocked via FIDO2. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ ARCHITECTURE.md for design details.
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
**Phase:** Phase 3 in progress. Step 17 complete, ready for Step 18.
|
**Phase:** Phase 3 in progress. Steps 17–18 complete, ready for Step 19.
|
||||||
|
|
||||||
**Last updated:** 2026-03-24
|
**Last updated:** 2026-03-24
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ ARCHITECTURE.md for design details.
|
|||||||
|
|
||||||
## Up Next
|
## Up Next
|
||||||
|
|
||||||
Step 18: FIDO2 support.
|
Step 19: Encryption CLI + Slot Management.
|
||||||
|
|
||||||
## Known Issues / Decisions Deferred
|
## Known Issues / Decisions Deferred
|
||||||
|
|
||||||
@@ -79,3 +79,4 @@ Step 18: FIDO2 support.
|
|||||||
| 2026-03-24 | — | JWT token auth implemented (transparent auto-renewal, XDG token cache, ReauthChallenge fast path). |
|
| 2026-03-24 | — | JWT token auth implemented (transparent auto-renewal, XDG token cache, ReauthChallenge fast path). |
|
||||||
| 2026-03-24 | — | Phase 3 encryption design: selective per-file encryption, KEK slots (passphrase + fido2/label), manifest-embedded config. |
|
| 2026-03-24 | — | Phase 3 encryption design: selective per-file encryption, KEK slots (passphrase + fido2/label), manifest-embedded config. |
|
||||||
| 2026-03-24 | 17 | Encryption core: Argon2id KEK, XChaCha20 DEK wrap/unwrap, selective per-file encrypt in Add/Checkpoint/Restore/Diff/Status. 10 tests. |
|
| 2026-03-24 | 17 | Encryption core: Argon2id KEK, XChaCha20 DEK wrap/unwrap, selective per-file encrypt in Add/Checkpoint/Restore/Diff/Status. 10 tests. |
|
||||||
|
| 2026-03-24 | 18 | FIDO2: FIDO2Device interface, AddFIDO2Slot, unlock resolution (fido2 first → passphrase fallback), mock device, 6 tests. |
|
||||||
|
|||||||
@@ -197,11 +197,10 @@ Depends on Steps 13, 14.
|
|||||||
|
|
||||||
Depends on Step 17.
|
Depends on Step 17.
|
||||||
|
|
||||||
- [ ] `garden/encrypt_fido2.go`: FIDO2 hmac-secret KEK derivation via libfido2
|
- [x] `garden/encrypt_fido2.go`: FIDO2Device interface, AddFIDO2Slot, unlockFIDO2, defaultFIDO2Label
|
||||||
- [ ] `garden/encrypt.go`: extend UnlockDEK to try fido2/* slots first (check credential_id against connected devices), fall back to passphrase
|
- [x] `garden/encrypt.go`: UnlockDEK tries fido2/* slots first (credential_id matching), falls back to passphrase
|
||||||
- [ ] `garden/encrypt.go`: `AddFIDO2Slot(label string) error` — unlock DEK via existing slot, register FIDO2 credential, wrap DEK, add slot to manifest
|
- [x] `garden/encrypt_fido2_test.go`: mock FIDO2 device, 6 tests (add slot, duplicate rejected, unlock via FIDO2, fallback to passphrase, persistence, encrypted round-trip with FIDO2)
|
||||||
- [ ] Tests: FIDO2 slot add/unwrap (may need mock or skip on CI without hardware)
|
- [x] Verify: `go test ./... && go vet ./... && golangci-lint run ./...`
|
||||||
- [ ] Verify: `go test ./... && go vet ./... && golangci-lint run ./...`
|
|
||||||
|
|
||||||
### Step 19: Encryption CLI + Slot Management
|
### Step 19: Encryption CLI + Slot Management
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ func (g *Garden) EncryptInit(passphrase string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnlockDEK attempts to unwrap the DEK using available KEK slots.
|
// UnlockDEK attempts to unwrap the DEK using available KEK slots.
|
||||||
// Tries passphrase slots (prompting via the provided function).
|
// Resolution order: try all fido2/* slots first (if a device is provided),
|
||||||
// The DEK is cached on the Garden for the duration of the command.
|
// then fall back to the passphrase slot. The DEK is cached on the Garden
|
||||||
func (g *Garden) UnlockDEK(promptPassphrase func() (string, error)) error {
|
// for the duration of the command.
|
||||||
|
func (g *Garden) UnlockDEK(promptPassphrase func() (string, error), fido2Device ...FIDO2Device) error {
|
||||||
if g.dek != nil {
|
if g.dek != nil {
|
||||||
return nil // already unlocked
|
return nil // already unlocked
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,14 @@ func (g *Garden) UnlockDEK(promptPassphrase func() (string, error)) error {
|
|||||||
return fmt.Errorf("encryption not initialized; run sgard encrypt init")
|
return fmt.Errorf("encryption not initialized; run sgard encrypt init")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try passphrase slot.
|
// 1. Try FIDO2 slots first.
|
||||||
|
if len(fido2Device) > 0 && fido2Device[0] != nil {
|
||||||
|
if g.unlockFIDO2(fido2Device[0]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to passphrase slot.
|
||||||
if slot, ok := enc.KekSlots["passphrase"]; ok {
|
if slot, ok := enc.KekSlots["passphrase"]; ok {
|
||||||
if promptPassphrase == nil {
|
if promptPassphrase == nil {
|
||||||
return fmt.Errorf("passphrase required but no prompt available")
|
return fmt.Errorf("passphrase required but no prompt available")
|
||||||
|
|||||||
161
garden/encrypt_fido2.go
Normal file
161
garden/encrypt_fido2.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package garden
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kisom/sgard/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIDO2Device abstracts the hardware interaction with a FIDO2 authenticator.
|
||||||
|
// The real implementation requires libfido2 (CGo); tests use a mock.
|
||||||
|
type FIDO2Device interface {
|
||||||
|
// Register creates a new credential with the hmac-secret extension.
|
||||||
|
// Returns the credential ID and the HMAC-secret output for the given salt.
|
||||||
|
Register(salt []byte) (credentialID []byte, hmacSecret []byte, err error)
|
||||||
|
|
||||||
|
// Derive computes HMAC(device_secret, salt) for an existing credential.
|
||||||
|
// Requires user touch.
|
||||||
|
Derive(credentialID []byte, salt []byte) (hmacSecret []byte, err error)
|
||||||
|
|
||||||
|
// Available reports whether a FIDO2 device is connected.
|
||||||
|
Available() bool
|
||||||
|
|
||||||
|
// MatchesCredential reports whether the connected device holds the
|
||||||
|
// given credential (by ID). This allows skipping devices that can't
|
||||||
|
// unwrap a particular slot without requiring a touch.
|
||||||
|
MatchesCredential(credentialID []byte) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFIDO2Slot adds a FIDO2 KEK slot to an encrypted repo. The DEK must
|
||||||
|
// already be unlocked (via passphrase or another FIDO2 slot). The label
|
||||||
|
// defaults to "fido2/<hostname>" but can be overridden.
|
||||||
|
func (g *Garden) AddFIDO2Slot(device FIDO2Device, label string) error {
|
||||||
|
if g.dek == nil {
|
||||||
|
return fmt.Errorf("DEK not unlocked; unlock via passphrase first")
|
||||||
|
}
|
||||||
|
if g.manifest.Encryption == nil {
|
||||||
|
return fmt.Errorf("encryption not initialized")
|
||||||
|
}
|
||||||
|
if !device.Available() {
|
||||||
|
return fmt.Errorf("no FIDO2 device connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize label.
|
||||||
|
if label == "" {
|
||||||
|
label = defaultFIDO2Label()
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(label, "fido2/") {
|
||||||
|
label = "fido2/" + label
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := g.manifest.Encryption.KekSlots[label]; exists {
|
||||||
|
return fmt.Errorf("slot %q already exists", label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate salt for this FIDO2 credential.
|
||||||
|
salt := make([]byte, saltSize)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return fmt.Errorf("generating salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register credential and get HMAC-secret (the KEK).
|
||||||
|
credID, kek, err := device.Register(salt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("FIDO2 registration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kek) < dekSize {
|
||||||
|
return fmt.Errorf("FIDO2 HMAC-secret too short: got %d bytes, need %d", len(kek), dekSize)
|
||||||
|
}
|
||||||
|
kek = kek[:dekSize]
|
||||||
|
|
||||||
|
// Wrap DEK with the FIDO2-derived KEK.
|
||||||
|
wrappedDEK, err := wrapDEK(g.dek, kek)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wrapping DEK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.manifest.Encryption.KekSlots[label] = &manifest.KekSlot{
|
||||||
|
Type: "fido2",
|
||||||
|
CredentialID: base64.StdEncoding.EncodeToString(credID),
|
||||||
|
Salt: base64.StdEncoding.EncodeToString(salt),
|
||||||
|
WrappedDEK: base64.StdEncoding.EncodeToString(wrappedDEK),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.manifest.Save(g.manifestPath); err != nil {
|
||||||
|
return fmt.Errorf("saving manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlockFIDO2 attempts to unlock the DEK using any available fido2/* slot.
|
||||||
|
// Returns true if successful.
|
||||||
|
func (g *Garden) unlockFIDO2(device FIDO2Device) bool {
|
||||||
|
if device == nil || !device.Available() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := g.manifest.Encryption
|
||||||
|
for name, slot := range enc.KekSlots {
|
||||||
|
if slot.Type != "fido2" || !strings.HasPrefix(name, "fido2/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
credID, err := base64.StdEncoding.DecodeString(slot.CredentialID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the connected device holds this credential.
|
||||||
|
if !device.MatchesCredential(credID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(slot.Salt)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kek, err := device.Derive(credID, salt)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(kek) < dekSize {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kek = kek[:dekSize]
|
||||||
|
|
||||||
|
wrappedDEK, err := base64.StdEncoding.DecodeString(slot.WrappedDEK)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dek, err := unwrapDEK(wrappedDEK, kek)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
g.dek = dek
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultFIDO2Label returns "<hostname>" as the default FIDO2 slot label.
|
||||||
|
func defaultFIDO2Label() string {
|
||||||
|
host, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "fido2/device"
|
||||||
|
}
|
||||||
|
// Use short hostname (before first dot).
|
||||||
|
if idx := strings.IndexByte(host, '.'); idx >= 0 {
|
||||||
|
host = host[:idx]
|
||||||
|
}
|
||||||
|
return "fido2/" + host
|
||||||
|
}
|
||||||
263
garden/encrypt_fido2_test.go
Normal file
263
garden/encrypt_fido2_test.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package garden
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockFIDO2 simulates a FIDO2 device for testing.
|
||||||
|
type mockFIDO2 struct {
|
||||||
|
deviceSecret []byte // fixed secret for HMAC derivation
|
||||||
|
credentials map[string]bool
|
||||||
|
available bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockFIDO2() *mockFIDO2 {
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(secret)
|
||||||
|
return &mockFIDO2{
|
||||||
|
deviceSecret: secret,
|
||||||
|
credentials: make(map[string]bool),
|
||||||
|
available: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFIDO2) Register(salt []byte) ([]byte, []byte, error) {
|
||||||
|
// Generate a random credential ID.
|
||||||
|
credID := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(credID)
|
||||||
|
m.credentials[string(credID)] = true
|
||||||
|
|
||||||
|
// Derive HMAC-secret.
|
||||||
|
mac := hmac.New(sha256.New, m.deviceSecret)
|
||||||
|
mac.Write(salt)
|
||||||
|
return credID, mac.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFIDO2) Derive(credentialID []byte, salt []byte) ([]byte, error) {
|
||||||
|
mac := hmac.New(sha256.New, m.deviceSecret)
|
||||||
|
mac.Write(salt)
|
||||||
|
return mac.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFIDO2) Available() bool {
|
||||||
|
return m.available
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFIDO2) MatchesCredential(credentialID []byte) bool {
|
||||||
|
return m.credentials[string(credentialID)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFIDO2Slot(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "test-key"); err != nil {
|
||||||
|
t.Fatalf("AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slot, ok := g.manifest.Encryption.KekSlots["fido2/test-key"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("fido2/test-key slot should exist")
|
||||||
|
}
|
||||||
|
if slot.Type != "fido2" {
|
||||||
|
t.Errorf("slot type = %s, want fido2", slot.Type)
|
||||||
|
}
|
||||||
|
if slot.CredentialID == "" {
|
||||||
|
t.Error("slot should have credential_id")
|
||||||
|
}
|
||||||
|
if slot.Salt == "" || slot.WrappedDEK == "" {
|
||||||
|
t.Error("slot should have salt and wrapped DEK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFIDO2SlotDuplicateRejected(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "mykey"); err != nil {
|
||||||
|
t.Fatalf("first AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.AddFIDO2Slot(device, "mykey"); err == nil {
|
||||||
|
t.Fatal("duplicate AddFIDO2Slot should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnlockViaFIDO2(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "test-key"); err != nil {
|
||||||
|
t.Fatalf("AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open (DEK not cached).
|
||||||
|
g2, err := Open(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock via FIDO2 — should succeed without passphrase prompt.
|
||||||
|
err = g2.UnlockDEK(nil, device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnlockDEK via FIDO2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g2.dek == nil {
|
||||||
|
t.Error("DEK should be cached after FIDO2 unlock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFIDO2FallbackToPassphrase(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "test-key"); err != nil {
|
||||||
|
t.Fatalf("AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open.
|
||||||
|
g2, err := Open(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIDO2 device is "unavailable" — should fall back to passphrase.
|
||||||
|
unavailable := newMockFIDO2()
|
||||||
|
unavailable.available = false
|
||||||
|
|
||||||
|
err = g2.UnlockDEK(
|
||||||
|
func() (string, error) { return "passphrase", nil },
|
||||||
|
unavailable,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnlockDEK fallback to passphrase: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFIDO2SlotPersists(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "test-key"); err != nil {
|
||||||
|
t.Fatalf("AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open and verify slot persisted.
|
||||||
|
g2, err := Open(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := g2.manifest.Encryption.KekSlots["fido2/test-key"]; !ok {
|
||||||
|
t.Fatal("FIDO2 slot should persist after re-open")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedRoundTripWithFIDO2(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EncryptInit("passphrase"); err != nil {
|
||||||
|
t.Fatalf("EncryptInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device := newMockFIDO2()
|
||||||
|
if err := g.AddFIDO2Slot(device, "test-key"); err != nil {
|
||||||
|
t.Fatalf("AddFIDO2Slot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an encrypted file.
|
||||||
|
content := []byte("fido2-protected secret\n")
|
||||||
|
secretFile := filepath.Join(root, "secret")
|
||||||
|
if err := os.WriteFile(secretFile, content, 0o600); err != nil {
|
||||||
|
t.Fatalf("writing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{secretFile}, true); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open, unlock via FIDO2, restore.
|
||||||
|
g2, err := Open(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g2.UnlockDEK(nil, device); err != nil {
|
||||||
|
t.Fatalf("UnlockDEK: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.Remove(secretFile)
|
||||||
|
if err := g2.Restore(nil, true, nil); err != nil {
|
||||||
|
t.Fatalf("Restore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := os.ReadFile(secretFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading restored: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(content) {
|
||||||
|
t.Errorf("content = %q, want %q", got, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user