HardwareFIDO2 implements FIDO2Device via go-libfido2 (CGo bindings to Yubico's libfido2). Gated behind //go:build fido2 tag to keep default builds CGo-free. Nix flake adds sgard-fido2 package variant. CLI changes: --fido2-pin flag, unlockDEK helper tries FIDO2 first, add-fido2/encrypt init --fido2 use real hardware, auto-unlock added to restore/checkpoint/diff for encrypted entries. Tested manually: add-fido2, add --encrypt, restore, checkpoint, diff all work with hardware FIDO2 key (touch-to-unlock, no passphrase). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
215 lines
4.9 KiB
Go
215 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/kisom/sgard/garden"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var encryptCmd = &cobra.Command{
|
|
Use: "encrypt",
|
|
Short: "Manage encryption keys and slots",
|
|
}
|
|
|
|
var fido2InitFlag bool
|
|
|
|
var encryptInitCmd = &cobra.Command{
|
|
Use: "init",
|
|
Short: "Initialize encryption (creates DEK and passphrase slot)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
passphrase, err := promptPassphrase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := g.EncryptInit(passphrase); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Encryption initialized with passphrase slot.")
|
|
|
|
if fido2InitFlag {
|
|
device := garden.DetectHardwareFIDO2(fido2PinFlag)
|
|
if device == nil {
|
|
fmt.Println("No FIDO2 device detected. Run 'sgard encrypt add-fido2' when one is connected.")
|
|
} else {
|
|
fmt.Println("Touch your FIDO2 device to register...")
|
|
if err := g.AddFIDO2Slot(device, fido2LabelFlag); err != nil {
|
|
return fmt.Errorf("adding FIDO2 slot: %w", err)
|
|
}
|
|
fmt.Println("FIDO2 slot added.")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var fido2LabelFlag string
|
|
|
|
var addFido2Cmd = &cobra.Command{
|
|
Use: "add-fido2",
|
|
Short: "Add a FIDO2 KEK slot",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !g.HasEncryption() {
|
|
return fmt.Errorf("encryption not initialized; run sgard encrypt init first")
|
|
}
|
|
|
|
if err := unlockDEK(g); err != nil {
|
|
return err
|
|
}
|
|
|
|
device := garden.DetectHardwareFIDO2(fido2PinFlag)
|
|
if device == nil {
|
|
return fmt.Errorf("no FIDO2 device detected; connect a FIDO2 key and try again")
|
|
}
|
|
|
|
fmt.Println("Touch your FIDO2 device to register...")
|
|
if err := g.AddFIDO2Slot(device, fido2LabelFlag); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("FIDO2 slot added.")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var removeSlotCmd = &cobra.Command{
|
|
Use: "remove-slot <name>",
|
|
Short: "Remove a KEK slot",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := g.RemoveSlot(args[0]); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Removed slot %q.\n", args[0])
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var listSlotsCmd = &cobra.Command{
|
|
Use: "list-slots",
|
|
Short: "List all KEK slots",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
slots := g.ListSlots()
|
|
if len(slots) == 0 {
|
|
fmt.Println("No encryption configured.")
|
|
return nil
|
|
}
|
|
|
|
// Sort for consistent output.
|
|
names := make([]string, 0, len(slots))
|
|
for name := range slots {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
fmt.Printf("%-30s %s\n", name, slots[name])
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var changePassphraseCmd = &cobra.Command{
|
|
Use: "change-passphrase",
|
|
Short: "Change the passphrase for the passphrase KEK slot",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !g.HasEncryption() {
|
|
return fmt.Errorf("encryption not initialized")
|
|
}
|
|
|
|
// Unlock with current credentials.
|
|
if err := unlockDEK(g); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get new passphrase.
|
|
fmt.Println("Enter new passphrase:")
|
|
newPassphrase, err := promptPassphrase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := g.ChangePassphrase(newPassphrase); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Passphrase changed.")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var rotateDEKCmd = &cobra.Command{
|
|
Use: "rotate-dek",
|
|
Short: "Generate a new DEK and re-encrypt all encrypted blobs",
|
|
Long: "Generates a new data encryption key, re-encrypts all encrypted blobs, and re-wraps the DEK with all KEK slots. Required when the DEK is suspected compromised.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
g, err := garden.Open(repoFlag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !g.HasEncryption() {
|
|
return fmt.Errorf("encryption not initialized")
|
|
}
|
|
|
|
// Unlock with current credentials.
|
|
if err := unlockDEK(g); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rotate — re-prompts for passphrase to re-wrap slot.
|
|
fmt.Println("Enter passphrase to re-wrap DEK:")
|
|
device := garden.DetectHardwareFIDO2(fido2PinFlag)
|
|
if err := g.RotateDEK(promptPassphrase, device); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("DEK rotated. All encrypted blobs re-encrypted.")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
encryptInitCmd.Flags().BoolVar(&fido2InitFlag, "fido2", false, "also register a FIDO2 hardware key")
|
|
addFido2Cmd.Flags().StringVar(&fido2LabelFlag, "label", "", "slot label (default: fido2/<hostname>)")
|
|
|
|
encryptCmd.AddCommand(encryptInitCmd)
|
|
encryptCmd.AddCommand(addFido2Cmd)
|
|
encryptCmd.AddCommand(removeSlotCmd)
|
|
encryptCmd.AddCommand(listSlotsCmd)
|
|
encryptCmd.AddCommand(changePassphraseCmd)
|
|
encryptCmd.AddCommand(rotateDEKCmd)
|
|
|
|
rootCmd.AddCommand(encryptCmd)
|
|
}
|