Files
arca/cmd/create.go
Kyle Isom 293bcdcdae Add create command for LUKS volume provisioning and Makefile
New `arca create <device> <alias>` command that formats a device with
LUKS encryption, creates a filesystem, optionally enrolls a FIDO2
keyslot, and adds the device to the arca config.

Adds Makefile with arca, vet, lint, test, and clean targets.

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

130 lines
3.5 KiB
Go

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 <device> <alias>",
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
}