package cli import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/afero" "github.com/spf13/cobra" ) // Import from init.go // ... existing imports ... func newUnlockersCmd() *cobra.Command { cmd := &cobra.Command{ Use: "unlockers", Short: "Manage unlockers", Long: `Create, list, and remove unlockers for the current vault.`, } cmd.AddCommand(newUnlockersListCmd()) cmd.AddCommand(newUnlockersAddCmd()) cmd.AddCommand(newUnlockersRmCmd()) return cmd } func newUnlockersListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List unlockers in the current vault", RunE: func(cmd *cobra.Command, args []string) error { jsonOutput, _ := cmd.Flags().GetBool("json") cli := NewCLIInstance() return cli.UnlockersList(jsonOutput) }, } cmd.Flags().Bool("json", false, "Output in JSON format") return cmd } func newUnlockersAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add ", Short: "Add a new unlocker", Long: `Add a new unlocker of the specified type (passphrase, keychain, pgp).`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.UnlockersAdd(args[0], cmd) }, } cmd.Flags().String("keyid", "", "GPG key ID for PGP unlockers") return cmd } func newUnlockersRmCmd() *cobra.Command { return &cobra.Command{ Use: "rm ", Short: "Remove an unlocker", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.UnlockersRemove(args[0]) }, } } func newUnlockerCmd() *cobra.Command { cmd := &cobra.Command{ Use: "unlocker", Short: "Manage current unlocker", Long: `Select the current unlocker for operations.`, } cmd.AddCommand(newUnlockerSelectSubCmd()) return cmd } func newUnlockerSelectSubCmd() *cobra.Command { return &cobra.Command{ Use: "select ", Short: "Select an unlocker as current", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.UnlockerSelect(args[0]) }, } } // UnlockersList lists unlockers in the current vault func (cli *CLIInstance) UnlockersList(jsonOutput bool) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } // Get the metadata first unlockerMetadataList, err := vlt.ListUnlockers() if err != nil { return err } // Load actual unlocker objects to get the proper IDs type UnlockerInfo struct { ID string `json:"id"` Type string `json:"type"` CreatedAt time.Time `json:"created_at"` Flags []string `json:"flags,omitempty"` } var unlockers []UnlockerInfo for _, metadata := range unlockerMetadataList { // Create unlocker instance to get the proper ID vaultDir, err := vlt.GetDirectory() if err != nil { continue } // Find the unlocker directory by type and created time unlockersDir := filepath.Join(vaultDir, "unlockers.d") files, err := afero.ReadDir(cli.fs, unlockersDir) if err != nil { continue } var unlocker secret.Unlocker for _, file := range files { if !file.IsDir() { continue } unlockerDir := filepath.Join(unlockersDir, file.Name()) metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json") // Check if this is the right unlocker by comparing metadata metadataBytes, err := afero.ReadFile(cli.fs, metadataPath) if err != nil { continue } var diskMetadata secret.UnlockerMetadata if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil { continue } // Match by type and creation time if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) { // Create the appropriate unlocker instance switch metadata.Type { case "passphrase": unlocker = secret.NewPassphraseUnlocker(cli.fs, unlockerDir, diskMetadata) case "keychain": unlocker = secret.NewKeychainUnlocker(cli.fs, unlockerDir, diskMetadata) case "pgp": unlocker = secret.NewPGPUnlocker(cli.fs, unlockerDir, diskMetadata) } break } } // Get the proper ID using the unlocker's ID() method var properID string if unlocker != nil { properID = unlocker.GetID() } else { properID = metadata.ID // fallback to metadata ID } unlockerInfo := UnlockerInfo{ ID: properID, Type: metadata.Type, CreatedAt: metadata.CreatedAt, Flags: metadata.Flags, } unlockers = append(unlockers, unlockerInfo) } if jsonOutput { // JSON output output := map[string]interface{}{ "unlockers": unlockers, } jsonBytes, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } fmt.Println(string(jsonBytes)) } else { // Pretty table output if len(unlockers) == 0 { fmt.Println("No unlockers found in current vault.") fmt.Println("Run 'secret unlockers add passphrase' to create one.") return nil } fmt.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS") fmt.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----") for _, unlocker := range unlockers { flags := "" if len(unlocker.Flags) > 0 { flags = strings.Join(unlocker.Flags, ",") } fmt.Printf("%-18s %-12s %-20s %s\n", unlocker.ID, unlocker.Type, unlocker.CreatedAt.Format("2006-01-02 15:04:05"), flags) } fmt.Printf("\nTotal: %d unlocker(s)\n", len(unlockers)) } return nil } // UnlockersAdd adds a new unlocker func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error { switch unlockerType { case "passphrase": // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return fmt.Errorf("failed to get current vault: %w", err) } // Try to unlock the vault if not already unlocked if vlt.Locked() { _, err := vlt.UnlockVault() if err != nil { return fmt.Errorf("failed to unlock vault: %w", err) } } // Check if passphrase is set in environment variable var passphraseStr string if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" { passphraseStr = envPassphrase } else { // Use secure passphrase input with confirmation passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ") if err != nil { return fmt.Errorf("failed to read passphrase: %w", err) } } passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr) if err != nil { return err } cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID()) return nil case "keychain": keychainUnlocker, err := secret.CreateKeychainUnlocker(cli.fs, cli.stateDir) if err != nil { return fmt.Errorf("failed to create macOS Keychain unlocker: %w", err) } cmd.Printf("Created macOS Keychain unlocker: %s\n", keychainUnlocker.GetID()) if keyName, err := keychainUnlocker.GetKeychainItemName(); err == nil { cmd.Printf("Keychain Item Name: %s\n", keyName) } return nil case "pgp": // Get GPG key ID from flag or environment variable var gpgKeyID string if flagKeyID, _ := cmd.Flags().GetString("keyid"); flagKeyID != "" { gpgKeyID = flagKeyID } else if envKeyID := os.Getenv(secret.EnvGPGKeyID); envKeyID != "" { gpgKeyID = envKeyID } else { return fmt.Errorf("GPG key ID required: use --keyid flag or set SB_GPG_KEY_ID environment variable") } pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir, gpgKeyID) if err != nil { return err } cmd.Printf("Created PGP unlocker: %s\n", pgpUnlocker.GetID()) cmd.Printf("GPG Key ID: %s\n", gpgKeyID) return nil default: return fmt.Errorf("unsupported unlocker type: %s (supported: passphrase, keychain, pgp)", unlockerType) } } // UnlockersRemove removes an unlocker func (cli *CLIInstance) UnlockersRemove(unlockerID string) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } return vlt.RemoveUnlocker(unlockerID) } // UnlockerSelect selects an unlocker as current func (cli *CLIInstance) UnlockerSelect(unlockerID string) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } return vlt.SelectUnlocker(unlockerID) }