324 lines
8.0 KiB
Go
324 lines
8.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func newKeysCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "keys",
|
|
Short: "Manage unlock keys",
|
|
Long: `Create, list, and remove unlock keys for the current vault.`,
|
|
}
|
|
|
|
cmd.AddCommand(newKeysListCmd())
|
|
cmd.AddCommand(newKeysAddCmd())
|
|
cmd.AddCommand(newKeysRmCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newKeysListCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List unlock keys in the current vault",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
cli := NewCLIInstance()
|
|
return cli.KeysList(jsonOutput)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().Bool("json", false, "Output in JSON format")
|
|
return cmd
|
|
}
|
|
|
|
func newKeysAddCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "add <type>",
|
|
Short: "Add a new unlock key",
|
|
Long: `Add a new unlock key of the specified type (passphrase, keychain, pgp).`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli := NewCLIInstance()
|
|
return cli.KeysAdd(args[0], cmd)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().String("keyid", "", "GPG key ID for PGP unlock keys")
|
|
return cmd
|
|
}
|
|
|
|
func newKeysRmCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "rm <key-id>",
|
|
Short: "Remove an unlock key",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli := NewCLIInstance()
|
|
return cli.KeysRemove(args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func newKeyCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "key",
|
|
Short: "Manage current unlock key",
|
|
Long: `Select the current unlock key for operations.`,
|
|
}
|
|
|
|
cmd.AddCommand(newKeySelectSubCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newKeySelectSubCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "select <key-id>",
|
|
Short: "Select an unlock key as current",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli := NewCLIInstance()
|
|
return cli.KeySelect(args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
// KeysList lists unlock keys in the current vault
|
|
func (cli *CLIInstance) KeysList(jsonOutput bool) error {
|
|
// Get current vault
|
|
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the metadata first
|
|
keyMetadataList, err := vault.ListUnlockKeys()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load actual unlock key objects to get the proper IDs
|
|
type KeyInfo struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Flags []string `json:"flags,omitempty"`
|
|
}
|
|
|
|
var keys []KeyInfo
|
|
for _, metadata := range keyMetadataList {
|
|
// Create unlock key instance to get the proper ID
|
|
vaultDir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Find the key directory by type and created time
|
|
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
|
files, err := afero.ReadDir(cli.fs, unlockKeysDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var unlockKey secret.UnlockKey
|
|
for _, file := range files {
|
|
if !file.IsDir() {
|
|
continue
|
|
}
|
|
|
|
keyDir := filepath.Join(unlockKeysDir, file.Name())
|
|
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
|
|
|
// Check if this is the right key by comparing metadata
|
|
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var diskMetadata secret.UnlockKeyMetadata
|
|
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 unlock key instance
|
|
switch metadata.Type {
|
|
case "passphrase":
|
|
unlockKey = secret.NewPassphraseUnlockKey(cli.fs, keyDir, metadata)
|
|
case "keychain":
|
|
unlockKey = secret.NewKeychainUnlockKey(cli.fs, keyDir, metadata)
|
|
case "pgp":
|
|
unlockKey = secret.NewPGPUnlockKey(cli.fs, keyDir, metadata)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Get the proper ID using the unlock key's ID() method
|
|
var properID string
|
|
if unlockKey != nil {
|
|
properID = unlockKey.ID()
|
|
} else {
|
|
properID = metadata.ID // fallback to metadata ID
|
|
}
|
|
|
|
keyInfo := KeyInfo{
|
|
ID: properID,
|
|
Type: metadata.Type,
|
|
CreatedAt: metadata.CreatedAt,
|
|
Flags: metadata.Flags,
|
|
}
|
|
keys = append(keys, keyInfo)
|
|
}
|
|
|
|
if jsonOutput {
|
|
// JSON output
|
|
output := map[string]interface{}{
|
|
"keys": keys,
|
|
}
|
|
|
|
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(keys) == 0 {
|
|
fmt.Println("No unlock keys found in current vault.")
|
|
fmt.Println("Run 'secret keys add passphrase' to create one.")
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("%-18s %-12s %-20s %s\n", "KEY ID", "TYPE", "CREATED", "FLAGS")
|
|
fmt.Printf("%-18s %-12s %-20s %s\n", "------", "----", "-------", "-----")
|
|
|
|
for _, key := range keys {
|
|
flags := ""
|
|
if len(key.Flags) > 0 {
|
|
flags = strings.Join(key.Flags, ",")
|
|
}
|
|
fmt.Printf("%-18s %-12s %-20s %s\n",
|
|
key.ID,
|
|
key.Type,
|
|
key.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
flags)
|
|
}
|
|
|
|
fmt.Printf("\nTotal: %d unlock key(s)\n", len(keys))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// KeysAdd adds a new unlock key
|
|
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
|
switch keyType {
|
|
case "passphrase":
|
|
// Get current vault
|
|
vault, err := secret.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 vault.Locked() {
|
|
_, err := vault.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 unlock key: ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
|
}
|
|
}
|
|
|
|
passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Created passphrase unlock key: %s\n", passphraseKey.GetMetadata().ID)
|
|
return nil
|
|
|
|
case "keychain":
|
|
keychainKey, err := secret.CreateKeychainUnlockKey(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create macOS Keychain unlock key: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Created macOS Keychain unlock key: %s\n", keychainKey.GetMetadata().ID)
|
|
if keyName, err := keychainKey.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")
|
|
}
|
|
|
|
pgpKey, err := secret.CreatePGPUnlockKey(cli.fs, cli.stateDir, gpgKeyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Created PGP unlock key: %s\n", pgpKey.GetMetadata().ID)
|
|
cmd.Printf("GPG Key ID: %s\n", gpgKeyID)
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported key type: %s (supported: passphrase, keychain, pgp)", keyType)
|
|
}
|
|
}
|
|
|
|
// KeysRemove removes an unlock key
|
|
func (cli *CLIInstance) KeysRemove(keyID string) error {
|
|
// Get current vault
|
|
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return vault.RemoveUnlockKey(keyID)
|
|
}
|
|
|
|
// KeySelect selects an unlock key as current
|
|
func (cli *CLIInstance) KeySelect(keyID string) error {
|
|
// Get current vault
|
|
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return vault.SelectUnlockKey(keyID)
|
|
}
|