package cmd import ( "fmt" "os" "git.wntrmute.dev/kyle/arca/internal/config" "git.wntrmute.dev/kyle/arca/internal/cryptsetup" "github.com/spf13/cobra" "golang.org/x/term" ) var createFsType string var createCmd = &cobra.Command{ Use: "create ", Short: "Create a new LUKS-encrypted volume", Long: "Formats a device with LUKS encryption, creates a filesystem, and adds it to the arca config.", Args: cobra.ExactArgs(2), RunE: runCreate, } func init() { createCmd.Flags().StringVarP(&createFsType, "filesystem", "t", "ext3", "filesystem type for mkfs") rootCmd.AddCommand(createCmd) } func runCreate(cmd *cobra.Command, args []string) error { device := args[0] alias := args[1] cfg := config.Load() if _, exists := cfg.Devices[alias]; exists { return fmt.Errorf("alias %q already in use", alias) } // Confirm destructive operation. fmt.Fprintf(os.Stderr, "This will ERASE all data on %s and create a new LUKS-encrypted volume.\n", device) fmt.Fprint(os.Stderr, "Continue? [y/N] ") var answer string fmt.Scanln(&answer) if answer != "y" && answer != "Y" { fmt.Fprintln(os.Stderr, "Aborted.") return nil } // Read passphrase with confirmation. fmt.Fprint(os.Stderr, "Passphrase: ") pass, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) if err != nil { return fmt.Errorf("reading passphrase: %w", err) } fmt.Fprint(os.Stderr, "Confirm passphrase: ") pass2, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) if err != nil { return fmt.Errorf("reading passphrase: %w", err) } if string(pass) != string(pass2) { return fmt.Errorf("passphrases do not match") } passphrase := string(pass) // Step 1: LUKS format. fmt.Fprintf(os.Stderr, "Formatting %s with LUKS...\n", device) if err := cryptsetup.LuksFormat(device, passphrase); err != nil { return err } // Step 2: Unlock. mapperName := cryptsetup.MapperName(device) fmt.Fprintf(os.Stderr, "Unlocking %s...\n", device) if err := cryptsetup.OpenWithPassphrase(device, mapperName, passphrase); err != nil { return err } // Step 3: Create filesystem. clearPath := "/dev/mapper/" + mapperName fmt.Fprintf(os.Stderr, "Creating %s filesystem on %s...\n", createFsType, clearPath) if err := cryptsetup.Mkfs(clearPath, createFsType); err != nil { cryptsetup.Close(mapperName) return err } // Step 4: Close the volume. if err := cryptsetup.Close(mapperName); err != nil { return err } // Step 5: Offer FIDO2 enrollment if a device is detected. methods := []string{"passphrase"} if output, err := cryptsetup.ListFIDO2Devices(); err == nil && output != "" { fmt.Fprintf(os.Stderr, "FIDO2 device detected:\n%s\n", output) fmt.Fprint(os.Stderr, "Enroll FIDO2 device as unlock method? [y/N] ") var fido2Answer string fmt.Scanln(&fido2Answer) if fido2Answer == "y" || fido2Answer == "Y" { if err := cryptsetup.EnrollFIDO2(device); err != nil { fmt.Fprintf(os.Stderr, "FIDO2 enrollment failed: %v\n", err) fmt.Fprintln(os.Stderr, "Continuing without FIDO2.") } else { methods = []string{"fido2", "passphrase"} fmt.Fprintln(os.Stderr, "FIDO2 enrolled successfully.") } } } // Step 6: Get UUID and add to config. uuid, err := cryptsetup.LuksUUID(device) if err != nil { return fmt.Errorf("reading UUID: %w", err) } cfg.Devices[alias] = config.DeviceConfig{ UUID: uuid, Methods: methods, } if err := cfg.Save(); err != nil { return fmt.Errorf("saving config: %w", err) } fmt.Printf("Created LUKS volume %s (UUID %s) as %q\n", device, uuid, alias) return nil }