Step 19: Encryption CLI, slot management, proto updates.
CLI: sgard encrypt init [--fido2], add-fido2 [--label], remove-slot, list-slots, change-passphrase. sgard add --encrypt flag with passphrase prompt for DEK unlock. Garden: RemoveSlot (refuses last slot), ListSlots, ChangePassphrase (re-wraps DEK with new passphrase, fresh salt). Proto: ManifestEntry gains encrypted + plaintext_hash fields. New KekSlot and Encryption messages. Manifest gains encryption field. server/convert.go: full round-trip conversion for encryption section including KekSlot map. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kisom/sgard/garden"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var encryptFlag bool
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add <path>...",
|
||||
Short: "Track files, directories, or symlinks",
|
||||
@@ -17,7 +22,16 @@ var addCmd = &cobra.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.Add(args); err != nil {
|
||||
if encryptFlag {
|
||||
if !g.HasEncryption() {
|
||||
return fmt.Errorf("encryption not initialized; run sgard encrypt init first")
|
||||
}
|
||||
if err := g.UnlockDEK(promptPassphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := g.Add(args, encryptFlag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,6 +40,16 @@ var addCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func promptPassphrase() (string, error) {
|
||||
fmt.Fprint(os.Stderr, "Passphrase: ")
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if scanner.Scan() {
|
||||
return strings.TrimSpace(scanner.Text()), nil
|
||||
}
|
||||
return "", fmt.Errorf("no passphrase provided")
|
||||
}
|
||||
|
||||
func init() {
|
||||
addCmd.Flags().BoolVar(&encryptFlag, "encrypt", false, "encrypt file contents before storing")
|
||||
rootCmd.AddCommand(addCmd)
|
||||
}
|
||||
|
||||
166
cmd/sgard/encrypt.go
Normal file
166
cmd/sgard/encrypt.go
Normal file
@@ -0,0 +1,166 @@
|
||||
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 {
|
||||
fmt.Println("FIDO2 support requires a hardware device implementation.")
|
||||
fmt.Println("Run 'sgard encrypt add-fido2' when a FIDO2 device is available.")
|
||||
}
|
||||
|
||||
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 := g.UnlockDEK(promptPassphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Real FIDO2 device implementation would go here.
|
||||
// For now, this is a placeholder that explains the requirement.
|
||||
return fmt.Errorf("FIDO2 hardware support not yet implemented; requires libfido2 binding")
|
||||
},
|
||||
}
|
||||
|
||||
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 passphrase.
|
||||
fmt.Println("Enter current passphrase:")
|
||||
if err := g.UnlockDEK(promptPassphrase); 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
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
encryptInitCmd.Flags().BoolVar(&fido2InitFlag, "fido2", false, "also set up FIDO2 (placeholder)")
|
||||
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)
|
||||
|
||||
rootCmd.AddCommand(encryptCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user