Replace direct golang.org/x/term calls with mcdsl/terminal across init, unseal, migrate-aad, and migrate-barrier commands. Seal password prompts use ReadPasswordBytes to preserve zeroization capability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.wntrmute.dev/mc/mcdsl/terminal"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/barrier"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/config"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/crypto"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/db"
|
|
)
|
|
|
|
var migrateBarrierCmd = &cobra.Command{
|
|
Use: "migrate-barrier",
|
|
Short: "Migrate barrier entries from v1 (MEK) to v2 (per-engine DEKs)",
|
|
Long: `Converts all v1 barrier entries to v2 format with per-engine data
|
|
encryption keys (DEKs). Creates a "system" DEK for non-engine data and
|
|
per-engine DEKs for each engine mount found in the barrier.
|
|
|
|
After migration, each engine's data is encrypted with its own DEK rather
|
|
than the MEK directly, limiting blast radius if a single key is compromised.
|
|
The MEK only wraps the DEKs.
|
|
|
|
Entries already in v2 format are skipped. Run while the server is stopped
|
|
to avoid concurrent access. Requires the unseal password.`,
|
|
RunE: runMigrateBarrier,
|
|
}
|
|
|
|
var migrateBarrierDryRun bool
|
|
|
|
func init() {
|
|
migrateBarrierCmd.Flags().BoolVar(&migrateBarrierDryRun, "dry-run", false, "report what would be migrated without writing")
|
|
rootCmd.AddCommand(migrateBarrierCmd)
|
|
}
|
|
|
|
func runMigrateBarrier(cmd *cobra.Command, args []string) error {
|
|
configPath := cfgFile
|
|
if configPath == "" {
|
|
configPath = "/srv/metacrypt/metacrypt.toml"
|
|
}
|
|
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
database, err := db.Open(cfg.Database.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = database.Close() }()
|
|
|
|
// Apply any pending schema migrations (creates barrier_keys table).
|
|
if err := db.Migrate(database); err != nil {
|
|
return fmt.Errorf("schema migration: %w", err)
|
|
}
|
|
|
|
// Read unseal password.
|
|
passwordBytes, err := terminal.ReadPasswordBytes("Unseal password: ")
|
|
if err != nil {
|
|
return fmt.Errorf("read password: %w", err)
|
|
}
|
|
defer crypto.Zeroize(passwordBytes)
|
|
|
|
// Load seal config and derive MEK.
|
|
mek, err := deriveMEK(database, passwordBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer crypto.Zeroize(mek)
|
|
|
|
if migrateBarrierDryRun {
|
|
return migrateBarrierDryRunReport(database, mek)
|
|
}
|
|
|
|
// Unseal the barrier with the MEK (loads existing DEKs).
|
|
b := barrier.NewAESGCMBarrier(database)
|
|
if err := b.Unseal(mek); err != nil {
|
|
return fmt.Errorf("unseal barrier: %w", err)
|
|
}
|
|
defer func() { _ = b.Seal() }()
|
|
|
|
ctx := context.Background()
|
|
migrated, err := b.MigrateToV2(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
|
|
if migrated == 0 {
|
|
fmt.Println("Nothing to migrate — all entries already use v2 format.")
|
|
} else {
|
|
fmt.Printf("Migrated %d entries to v2 format with per-engine DEKs.\n", migrated)
|
|
}
|
|
|
|
// Show key summary.
|
|
keys, err := b.ListKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list keys: %w", err)
|
|
}
|
|
if len(keys) > 0 {
|
|
fmt.Printf("\nBarrier keys (%d):\n", len(keys))
|
|
for _, k := range keys {
|
|
fmt.Printf(" %-30s version=%d created=%s\n", k.KeyID, k.Version, k.CreatedAt)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func migrateBarrierDryRunReport(database *sql.DB, mek []byte) error {
|
|
ctx := context.Background()
|
|
rows, err := database.QueryContext(ctx, "SELECT path, value FROM barrier_entries")
|
|
if err != nil {
|
|
return fmt.Errorf("query entries: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var v1Count, v2Count, total int
|
|
var v1Paths []string
|
|
for rows.Next() {
|
|
var path string
|
|
var value []byte
|
|
if err := rows.Scan(&path, &value); err != nil {
|
|
return fmt.Errorf("scan: %w", err)
|
|
}
|
|
total++
|
|
if len(value) > 0 && value[0] == crypto.BarrierVersionV2 {
|
|
v2Count++
|
|
} else {
|
|
v1Count++
|
|
v1Paths = append(v1Paths, path)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("iterate: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Total entries: %d\n", total)
|
|
fmt.Printf("Already v2: %d\n", v2Count)
|
|
fmt.Printf("Need migration: %d\n", v1Count)
|
|
|
|
if len(v1Paths) > 0 {
|
|
fmt.Println("\nEntries that would be migrated:")
|
|
for _, p := range v1Paths {
|
|
fmt.Printf(" %s\n", p)
|
|
}
|
|
} else {
|
|
fmt.Println("\nNothing to migrate.")
|
|
}
|
|
|
|
return nil
|
|
}
|