Fix vault creation to require mnemonic and set up initial unlocker
- Vault creation now prompts for mnemonic if not in environment - Automatically creates passphrase unlocker during vault creation - Prevents 'missing public key' error when adding secrets to new vaults - Updates tests to reflect new vault creation flow
This commit is contained in:
@@ -17,6 +17,23 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// UnlockerInfo represents unlocker information for display
|
||||
type UnlockerInfo struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
IsCurrent bool `json:"isCurrent"`
|
||||
}
|
||||
|
||||
// Table formatting constants
|
||||
const (
|
||||
unlockerIDWidth = 40
|
||||
unlockerTypeWidth = 12
|
||||
unlockerDateWidth = 20
|
||||
unlockerFlagsWidth = 20
|
||||
)
|
||||
|
||||
// getDefaultGPGKey returns the default GPG key ID if available
|
||||
func getDefaultGPGKey() (string, error) {
|
||||
// First try to get the configured default key using gpgconf
|
||||
@@ -94,22 +111,66 @@ func newUnlockerListCmd() *cobra.Command {
|
||||
func newUnlockerAddCmd() *cobra.Command {
|
||||
// Build the supported types list based on platform
|
||||
supportedTypes := "passphrase, pgp"
|
||||
typeDescriptions := `Available unlocker types:
|
||||
|
||||
passphrase - Traditional password-based encryption
|
||||
Prompts for a passphrase that will be used to encrypt/decrypt the vault's master key.
|
||||
The passphrase is never stored in plaintext.
|
||||
|
||||
pgp - GNU Privacy Guard (GPG) key-based encryption
|
||||
Uses your existing GPG key to encrypt/decrypt the vault's master key.
|
||||
Requires gpg to be installed and configured with at least one secret key.
|
||||
Use --keyid to specify a particular key, otherwise uses your default GPG key.`
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
supportedTypes = "passphrase, keychain, pgp"
|
||||
typeDescriptions = `Available unlocker types:
|
||||
|
||||
passphrase - Traditional password-based encryption
|
||||
Prompts for a passphrase that will be used to encrypt/decrypt the vault's master key.
|
||||
The passphrase is never stored in plaintext.
|
||||
|
||||
keychain - macOS Keychain integration (macOS only)
|
||||
Stores the vault's master key in the macOS Keychain, protected by your login password.
|
||||
Automatically unlocks when your Keychain is unlocked (e.g., after login).
|
||||
Provides seamless integration with macOS security features like Touch ID.
|
||||
|
||||
pgp - GNU Privacy Guard (GPG) key-based encryption
|
||||
Uses your existing GPG key to encrypt/decrypt the vault's master key.
|
||||
Requires gpg to be installed and configured with at least one secret key.
|
||||
Use --keyid to specify a particular key, otherwise uses your default GPG key.`
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <type>",
|
||||
Short: "Add a new unlocker",
|
||||
Long: fmt.Sprintf(`Add a new unlocker of the specified type (%s).
|
||||
Long: fmt.Sprintf(`Add a new unlocker to the current vault.
|
||||
|
||||
For PGP unlockers, you can optionally specify a GPG key ID with --keyid.
|
||||
If not specified, the default GPG key will be used.`, supportedTypes),
|
||||
Args: cobra.ExactArgs(1),
|
||||
%s
|
||||
|
||||
Each vault can have multiple unlockers, allowing different authentication methods
|
||||
to access the same vault. This provides flexibility and backup access options.`, typeDescriptions),
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgs: strings.Split(supportedTypes, ", "),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
unlockerType := args[0]
|
||||
|
||||
// Validate unlocker type
|
||||
validTypes := strings.Split(supportedTypes, ", ")
|
||||
valid := false
|
||||
for _, t := range validTypes {
|
||||
if unlockerType == t {
|
||||
valid = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid unlocker type '%s'\n\nSupported types: %s\n\n"+
|
||||
"Run 'secret unlocker add --help' for detailed descriptions", unlockerType, supportedTypes)
|
||||
}
|
||||
|
||||
// Check if --keyid was used with non-PGP type
|
||||
if unlockerType != "pgp" && cmd.Flags().Changed("keyid") {
|
||||
return fmt.Errorf("--keyid flag is only valid for PGP unlockers")
|
||||
@@ -125,6 +186,7 @@ If not specified, the default GPG key will be used.`, supportedTypes),
|
||||
}
|
||||
|
||||
func newUnlockerRemoveCmd() *cobra.Command {
|
||||
cli := NewCLIInstance()
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove <unlocker-id>",
|
||||
Aliases: []string{"rm"},
|
||||
@@ -132,7 +194,8 @@ func newUnlockerRemoveCmd() *cobra.Command {
|
||||
Long: `Remove an unlocker from the current vault. Cannot remove the last unlocker if the vault has ` +
|
||||
`secrets unless --force is used. Warning: Without unlockers and without your mnemonic, vault data ` +
|
||||
`will be permanently inaccessible.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: getUnlockerIDsCompletionFunc(cli.fs, cli.stateDir),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
cli := NewCLIInstance()
|
||||
@@ -147,10 +210,13 @@ func newUnlockerRemoveCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func newUnlockerSelectCmd() *cobra.Command {
|
||||
cli := NewCLIInstance()
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "select <unlocker-id>",
|
||||
Short: "Select an unlocker as current",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "select <unlocker-id>",
|
||||
Short: "Select an unlocker as current",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: getUnlockerIDsCompletionFunc(cli.fs, cli.stateDir),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
|
||||
@@ -167,6 +233,13 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the current unlocker ID
|
||||
var currentUnlockerID string
|
||||
currentUnlocker, err := vlt.GetCurrentUnlocker()
|
||||
if err == nil {
|
||||
currentUnlockerID = currentUnlocker.GetID()
|
||||
}
|
||||
|
||||
// Get the metadata first
|
||||
unlockerMetadataList, err := vlt.ListUnlockers()
|
||||
if err != nil {
|
||||
@@ -174,13 +247,6 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -246,49 +312,68 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
Type: metadata.Type,
|
||||
CreatedAt: metadata.CreatedAt,
|
||||
Flags: metadata.Flags,
|
||||
IsCurrent: properID == currentUnlockerID,
|
||||
}
|
||||
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 unlocker 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 cli.printUnlockersJSON(unlockers, currentUnlockerID)
|
||||
}
|
||||
|
||||
return cli.printUnlockersTable(unlockers)
|
||||
}
|
||||
|
||||
// printUnlockersJSON prints unlockers in JSON format
|
||||
func (cli *Instance) printUnlockersJSON(unlockers []UnlockerInfo, currentUnlockerID string) error {
|
||||
output := map[string]interface{}{
|
||||
"unlockers": unlockers,
|
||||
"currentUnlockerID": currentUnlockerID,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
cli.cmd.Println(string(jsonBytes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printUnlockersTable prints unlockers in a formatted table
|
||||
func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
||||
if len(unlockers) == 0 {
|
||||
cli.cmd.Println("No unlockers found in current vault.")
|
||||
cli.cmd.Println("Run 'secret unlocker add passphrase' to create one.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n",
|
||||
strings.Repeat("-", unlockerIDWidth), strings.Repeat("-", unlockerTypeWidth),
|
||||
strings.Repeat("-", unlockerDateWidth), strings.Repeat("-", unlockerFlagsWidth))
|
||||
|
||||
for _, unlocker := range unlockers {
|
||||
flags := ""
|
||||
if len(unlocker.Flags) > 0 {
|
||||
flags = strings.Join(unlocker.Flags, ",")
|
||||
}
|
||||
prefix := " "
|
||||
if unlocker.IsCurrent {
|
||||
prefix = "* "
|
||||
}
|
||||
cli.cmd.Printf("%s%-40s %-12s %-20s %s\n",
|
||||
prefix,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -331,6 +416,13 @@ func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error
|
||||
|
||||
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
|
||||
|
||||
// Auto-select the newly created unlocker
|
||||
if err := vlt.SelectUnlocker(passphraseUnlocker.GetID()); err != nil {
|
||||
cmd.Printf("Warning: Failed to auto-select new unlocker: %v\n", err)
|
||||
} else {
|
||||
cmd.Printf("Automatically selected as current unlocker\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case "keychain":
|
||||
@@ -348,6 +440,17 @@ func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error
|
||||
cmd.Printf("Keychain Item Name: %s\n", keyName)
|
||||
}
|
||||
|
||||
// Auto-select the newly created unlocker
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
if err := vlt.SelectUnlocker(keychainUnlocker.GetID()); err != nil {
|
||||
cmd.Printf("Warning: Failed to auto-select new unlocker: %v\n", err)
|
||||
} else {
|
||||
cmd.Printf("Automatically selected as current unlocker\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case "pgp":
|
||||
@@ -393,6 +496,13 @@ func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error
|
||||
cmd.Printf("Created PGP unlocker: %s\n", pgpUnlocker.GetID())
|
||||
cmd.Printf("GPG Key ID: %s\n", gpgKeyID)
|
||||
|
||||
// Auto-select the newly created unlocker
|
||||
if err := vlt.SelectUnlocker(pgpUnlocker.GetID()); err != nil {
|
||||
cmd.Printf("Warning: Failed to auto-select new unlocker: %v\n", err)
|
||||
} else {
|
||||
cmd.Printf("Automatically selected as current unlocker\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user