- Fix staticcheck QF1011: Remove explicit type declaration for io.Writer variables - Fix tagliatelle: Change all JSON tags from snake_case to camelCase - created_at → createdAt - keychain_item_name → keychainItemName - age_public_key → agePublicKey - age_priv_key_passphrase → agePrivKeyPassphrase - encrypted_longterm_key → encryptedLongtermKey - derivation_index → derivationIndex - public_key_hash → publicKeyHash - mnemonic_family_hash → mnemonicFamilyHash - gpg_key_id → gpgKeyId - Fix lll: Break long function signature line to stay under 120 character limit All linter issues have been resolved. The codebase now passes all linter checks.
337 lines
8.5 KiB
Go
337 lines
8.5 KiB
Go
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, _ []string) error {
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
cli := NewCLIInstance()
|
|
cli.cmd = cmd
|
|
|
|
return cli.UnlockersList(jsonOutput)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().Bool("json", false, "Output in JSON format")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newUnlockersAddCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "add <type>",
|
|
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 <unlocker-id>",
|
|
Short: "Remove an unlocker",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(_ *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 <unlocker-id>",
|
|
Short: "Select an unlocker as current",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(_ *cobra.Command, args []string) error {
|
|
cli := NewCLIInstance()
|
|
|
|
return cli.UnlockerSelect(args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
// UnlockersList lists unlockers in the current vault
|
|
func (cli *Instance) 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:"createdAt"`
|
|
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 // FIXME this error needs to be handled
|
|
}
|
|
|
|
var diskMetadata secret.UnlockerMetadata
|
|
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
|
continue // FIXME this error needs to be handled
|
|
}
|
|
|
|
// 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 {
|
|
// Generate ID as fallback
|
|
properID = fmt.Sprintf("%s-%s", metadata.CreatedAt.Format("2006-01-02.15.04"), metadata.Type)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
cli.cmd.Println(string(jsonBytes))
|
|
} else {
|
|
// Pretty table output
|
|
if len(unlockers) == 0 {
|
|
cli.cmd.Println("No unlockers found in current vault.")
|
|
cli.cmd.Println("Run 'secret unlockers add passphrase' to create one.")
|
|
|
|
return nil
|
|
}
|
|
|
|
cli.cmd.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
|
cli.cmd.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----")
|
|
|
|
for _, unlocker := range unlockers {
|
|
flags := ""
|
|
if len(unlocker.Flags) > 0 {
|
|
flags = strings.Join(unlocker.Flags, ",")
|
|
}
|
|
cli.cmd.Printf("%-18s %-12s %-20s %s\n",
|
|
unlocker.ID,
|
|
unlocker.Type,
|
|
unlocker.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
flags)
|
|
}
|
|
|
|
cli.cmd.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnlockersAdd adds a new unlocker
|
|
func (cli *Instance) 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)
|
|
}
|
|
|
|
// For passphrase unlockers, we don't need the vault to be unlocked
|
|
// The CreatePassphraseUnlocker method will handle getting the long-term key
|
|
|
|
// 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 *Instance) 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 *Instance) UnlockerSelect(unlockerID string) error {
|
|
// Get current vault
|
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return vlt.SelectUnlocker(unlockerID)
|
|
}
|