package cli import ( "fmt" "log/slog" "os" "path/filepath" "strings" "time" "filippo.io/age" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" ) func newInitCmd() *cobra.Command { return &cobra.Command{ Use: "init", Short: "Initialize the secrets manager", Long: `Create the necessary directory structure for storing secrets and generate encryption keys.`, RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.Init(cmd) }, } } // Init initializes the secrets manager func (cli *CLIInstance) Init(cmd *cobra.Command) error { secret.Debug("Starting secret manager initialization") // Create state directory stateDir := cli.GetStateDir() secret.DebugWith("Creating state directory", slog.String("path", stateDir)) if err := cli.fs.MkdirAll(stateDir, secret.DirPerms); err != nil { secret.Debug("Failed to create state directory", "error", err) return fmt.Errorf("failed to create state directory: %w", err) } if cmd != nil { cmd.Printf("Initialized secrets manager at: %s\n", stateDir) } // Prompt for mnemonic var mnemonicStr string if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" { secret.Debug("Using mnemonic from environment variable") mnemonicStr = envMnemonic } else { secret.Debug("Prompting user for mnemonic phrase") // Read mnemonic from stdin using shared line reader var err error mnemonicStr, err = readLineFromStdin("Enter your BIP39 mnemonic phrase: ") if err != nil { secret.Debug("Failed to read mnemonic from stdin", "error", err) return fmt.Errorf("failed to read mnemonic: %w", err) } } if mnemonicStr == "" { secret.Debug("Empty mnemonic provided") return fmt.Errorf("mnemonic cannot be empty") } // Validate the mnemonic using BIP39 secret.DebugWith("Validating BIP39 mnemonic", slog.Int("word_count", len(strings.Fields(mnemonicStr)))) if !bip39.IsMnemonicValid(mnemonicStr) { secret.Debug("Invalid BIP39 mnemonic provided") return fmt.Errorf("invalid BIP39 mnemonic phrase\nRun 'secret generate mnemonic' to create a valid mnemonic") } // Calculate mnemonic hash for index tracking mnemonicHash := vault.ComputeDoubleSHA256([]byte(mnemonicStr)) secret.DebugWith("Calculated mnemonic hash", slog.String("hash", mnemonicHash)) // Get the next available derivation index for this mnemonic derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonicHash) if err != nil { secret.Debug("Failed to get next derivation index", "error", err) return fmt.Errorf("failed to get next derivation index: %w", err) } secret.DebugWith("Using derivation index", slog.Uint64("index", uint64(derivationIndex))) // Derive long-term keypair from mnemonic with the appropriate index secret.DebugWith("Deriving long-term key from mnemonic", slog.Uint64("index", uint64(derivationIndex))) ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, derivationIndex) if err != nil { secret.Debug("Failed to derive long-term key", "error", err) return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) } // Calculate the long-term key hash ltKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.String())) secret.DebugWith("Calculated long-term key hash", slog.String("hash", ltKeyHash)) // Create the default vault secret.Debug("Creating default vault") vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default") if err != nil { secret.Debug("Failed to create default vault", "error", err) return fmt.Errorf("failed to create default vault: %w", err) } // Set as current vault secret.Debug("Setting default vault as current") if err := vault.SelectVault(cli.fs, cli.stateDir, "default"); err != nil { secret.Debug("Failed to select default vault", "error", err) return fmt.Errorf("failed to select default vault: %w", err) } // Store long-term public key in vault vaultDir := filepath.Join(stateDir, "vaults.d", "default") ltPubKey := ltIdentity.Recipient().String() secret.DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir)) if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), secret.FilePerms); err != nil { secret.Debug("Failed to write long-term public key", "error", err) return fmt.Errorf("failed to write long-term public key: %w", err) } // Save vault metadata metadata := &vault.VaultMetadata{ Name: "default", CreatedAt: time.Now(), DerivationIndex: derivationIndex, LongTermKeyHash: ltKeyHash, MnemonicHash: mnemonicHash, } if err := vault.SaveVaultMetadata(cli.fs, vaultDir, metadata); err != nil { secret.Debug("Failed to save vault metadata", "error", err) return fmt.Errorf("failed to save vault metadata: %w", err) } secret.Debug("Saved vault metadata with derivation index and key hash") // Unlock the vault with the derived long-term key vlt.Unlock(ltIdentity) // Prompt for passphrase for unlocker var passphraseStr string if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" { secret.Debug("Using unlock passphrase from environment variable") passphraseStr = envPassphrase } else { secret.Debug("Prompting user for unlock passphrase") // Use secure passphrase input with confirmation passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ") if err != nil { secret.Debug("Failed to read unlock passphrase", "error", err) return fmt.Errorf("failed to read passphrase: %w", err) } } // Create passphrase-protected unlocker secret.Debug("Creating passphrase-protected unlocker") passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr) if err != nil { secret.Debug("Failed to create unlocker", "error", err) return fmt.Errorf("failed to create unlocker: %w", err) } // Encrypt long-term private key to the unlocker unlockerDir := passphraseUnlocker.GetDirectory() // Read unlocker public key unlockerPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockerDir, "pub.age")) if err != nil { return fmt.Errorf("failed to read unlocker public key: %w", err) } unlockerRecipient, err := age.ParseX25519Recipient(string(unlockerPubKeyData)) if err != nil { return fmt.Errorf("failed to parse unlocker public key: %w", err) } // Encrypt long-term private key to unlocker ltPrivKeyData := []byte(ltIdentity.String()) encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockerRecipient) if err != nil { return fmt.Errorf("failed to encrypt long-term private key: %w", err) } // Write encrypted long-term private key if err := afero.WriteFile(cli.fs, filepath.Join(unlockerDir, "longterm.age"), encryptedLtPrivKey, secret.FilePerms); err != nil { return fmt.Errorf("failed to write encrypted long-term private key: %w", err) } if cmd != nil { cmd.Printf("\nDefault vault created and configured\n") cmd.Printf("Long-term public key: %s\n", ltPubKey) cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID()) cmd.Println("\nYour secret manager is ready to use!") cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,") cmd.Println("unlockers are not required for secret operations.") } return nil } // readSecurePassphrase reads a passphrase securely from the terminal without echoing // This version adds confirmation (read twice) for creating new unlockers func readSecurePassphrase(prompt string) (string, error) { // Get the first passphrase passphrase1, err := secret.ReadPassphrase(prompt) if err != nil { return "", err } // Read confirmation passphrase passphrase2, err := secret.ReadPassphrase("Confirm passphrase: ") if err != nil { return "", fmt.Errorf("failed to read passphrase confirmation: %w", err) } // Compare passphrases if passphrase1 != passphrase2 { return "", fmt.Errorf("passphrases do not match") } return passphrase1, nil }