Update golangci-lint to v2.12.2 with canonical config (#29)
All checks were successful
check / check (push) Successful in 43s
All checks were successful
check / check (push) Successful in 43s
Bumps golangci-lint from v2.1.6 (digest-only pin in the `Dockerfile` lint stage) to v2.12.2, pinned by tag and digest (Debian-based image). Replaces `.golangci.yml` with the canonical strict config: all linters enabled except the standard disable list (`exhaustruct`, `depguard`, `godot`, `wsl`, `wrapcheck`, `varnamelen`), `lll` at 88, `funlen` 80/50, `cyclop` 15, `dupl` 100, and test files are now linted (the old config had `tests: false`, an enable-only list of ~20 linters, `lll` 120, and a blanket exclusion of `internal/macse`). The stricter config surfaced ~1550 findings, all fixed: - `wsl_v5` (439) / `nlreturn` (24): blank-line insertions - `lll` (309): line wrapping at 88 columns; long literals split with `+` concatenation, values unchanged - `noinlineerr` (130): `if err := ...` split into assignment plus check - `paralleltest` (116): `t.Parallel()` added to tests without shared state; reasoned `//nolint` where `t.Setenv` or shared fixtures forbid it - `err113` (97): package-level sentinel errors (new `internal/vault/errors.go`), `%w` wrapping, `errors.Is` - `perfsprint` (74) / `modernize` (39) / `intrange`: `strconv`, `errors.New`, `slices.Contains`, `any`, `SplitSeq` - `goconst` (40) / `dupword` (41) / `testifylint` (42) / `thelper` (33): constants, assertion fixes, `t.Helper()` - `noctx` (22): `exec.CommandContext` for gpg/CLI invocations - `testpackage` (18): black-box tests moved to `_test` packages where they use only exported identifiers; white-box files carry a reasoned `//nolint` - `funlen`/`cyclop`/`gocognit`/`nestif`/`dupl`: behavior-preserving helper extraction - assorted singletons: `gosec`, `gosmopolitan`, `funcorder`, `nonamedreturns`, `makezero`, `prealloc`, `godox`, `nolintlint`, `ireturn`, `nilnil`, `gochecknoinits` ## User-visible strings **None changed.** Every error message this branch composes is byte-identical to the one `main` composes. The `err113` sentinels are shaped so `fmt.Errorf` reassembles the original text around them: the sentinel carries the fixed words and the caller supplies the interpolated value in the position it has always occupied. Where the value sits mid-sentence the sentinel holds only a fragment (e.g. `vault.ErrVaultNotFound` is `"does not exist"`, composed by its caller as `vault <name> does not exist`); each such sentinel documents the message it participates in. Verified mechanically, not by inspection: every `fmt.Errorf` and `errors.New` call site in both trees is parsed, the `Error()` text of any sentinel passed to `%w` is substituted in, and the resulting sets of composed message templates are compared. All 350 templates `main` produces are still produced, character for character. The set of lost or altered messages is empty. ## `unlocker list` `findUnlockerIDByMetadata` returns `(string, error)` rather than signalling failure with an empty ID, so an unreadable `unlockers.d` is no longer indistinguishable from "no matching entry". `UnlockersList` skips such an entry with a warning naming the directory — its behavior before the scan was extracted into a helper — instead of emitting a row under a synthesized fallback ID that no `unlocker remove` or `unlocker select` can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by `internal/cli/unlockers_list_test.go`. `TODO.md` records the change plus follow-ups (version-completion TODOs formerly in code comments, darwin-gated files exceeding 88 columns that Linux CI does not lint). `make check` is green and the pinned v2.12.2 image reports `0 issues.` Note the test suite needs the memlock ulimit from `script/cibuild` for the 10MB memguard test; that requirement is pre-existing. Not changed: `script/bootstrap` installs golangci-lint via the system package manager (no version pin to bump), and `script/lint` invokes whatever `golangci-lint` is on PATH. golangci-lint v2.12 deprecates `gomodguard` in favor of `gomodguard_v2` (warning only); the canonical config owns that decision. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #29 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #29.
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +21,37 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Unlocker type names and platform identifiers shared across the CLI
|
||||
const (
|
||||
unlockerTypePassphrase = "passphrase"
|
||||
unlockerTypeKeychain = "keychain"
|
||||
unlockerTypePGP = "pgp"
|
||||
unlockerTypeSecureEnclave = "secure-enclave"
|
||||
|
||||
platformDarwin = "darwin"
|
||||
|
||||
cmdUseList = "list"
|
||||
)
|
||||
|
||||
// Sentinel errors for unlocker operations
|
||||
var (
|
||||
errNoGPGSecretKeys = errors.New("no GPG secret keys found")
|
||||
errInvalidUnlockerType = errors.New("invalid unlocker type")
|
||||
errKeyIDOnlyForPGP = errors.New(
|
||||
"--keyid flag is only valid for PGP unlockers")
|
||||
errKeychainMacOSOnly = errors.New(
|
||||
"keychain unlockers are only supported on macOS")
|
||||
errSecureEnclaveMacOSOnly = errors.New(
|
||||
"secure enclave unlockers are only supported on macOS")
|
||||
// errGPGKeyAlreadyUnlocker carries only the message tail; the caller
|
||||
// composes "GPG key <id> is already added as an unlocker".
|
||||
errGPGKeyAlreadyUnlocker = errors.New(
|
||||
"is already added as an unlocker")
|
||||
errUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
||||
errLastUnlocker = errors.New("refusing to remove last unlocker")
|
||||
errUnlockerExists = errors.New("unlocker already exists")
|
||||
)
|
||||
|
||||
// UnlockerInfo represents unlocker information for display
|
||||
type UnlockerInfo struct {
|
||||
ID string `json:"id"`
|
||||
@@ -37,12 +71,14 @@ const (
|
||||
|
||||
// getDefaultGPGKey returns the default GPG key ID if available
|
||||
func getDefaultGPGKey() (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// First try to get the configured default key using gpgconf
|
||||
cmd := exec.Command("gpgconf", "--list-options", "gpg")
|
||||
cmd := exec.CommandContext(ctx, "gpgconf", "--list-options", "gpg")
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
for line := range strings.SplitSeq(string(output), "\n") {
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) > 9 && fields[0] == "default-key" && fields[9] != "" {
|
||||
// The default key is in field 10 (index 9)
|
||||
@@ -52,15 +88,15 @@ func getDefaultGPGKey() (string, error) {
|
||||
}
|
||||
|
||||
// If no default key is configured, get the first secret key
|
||||
cmd = exec.Command("gpg", "--list-secret-keys", "--with-colons")
|
||||
cmd = exec.CommandContext(ctx, "gpg", "--list-secret-keys", "--with-colons")
|
||||
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list GPG keys: %w", err)
|
||||
}
|
||||
|
||||
// Parse output to find the first usable secret key
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
for line := range strings.SplitSeq(string(output), "\n") {
|
||||
// sec line indicates a secret key
|
||||
if strings.HasPrefix(line, "sec:") {
|
||||
fields := strings.Split(line, ":")
|
||||
@@ -71,7 +107,7 @@ func getDefaultGPGKey() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no GPG secret keys found")
|
||||
return "", errNoGPGSecretKeys
|
||||
}
|
||||
|
||||
func newUnlockerCmd() *cobra.Command {
|
||||
@@ -91,7 +127,7 @@ func newUnlockerCmd() *cobra.Command {
|
||||
|
||||
func newUnlockerListCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdUseList,
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List unlockers in the current vault",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
@@ -101,6 +137,7 @@ func newUnlockerListCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize CLI: %w", err)
|
||||
}
|
||||
|
||||
cli.cmd = cmd
|
||||
|
||||
return cli.UnlockersList(jsonOutput)
|
||||
@@ -112,53 +149,80 @@ func newUnlockerListCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newUnlockerAddCmd() *cobra.Command {
|
||||
// unlockerAddHelp returns the supported unlocker types list and their
|
||||
// descriptions for the current platform
|
||||
func unlockerAddHelp() (string, string) {
|
||||
// Build the supported types list based on platform
|
||||
supportedTypes := "passphrase, pgp"
|
||||
typeDescriptions := `Available unlocker types:
|
||||
typeDescriptions := "Available unlocker types:\n" +
|
||||
"\n" +
|
||||
" passphrase - Traditional password-based encryption\n" +
|
||||
" Prompts for a passphrase that will be used to " +
|
||||
"encrypt/decrypt the vault's master key.\n" +
|
||||
" The passphrase is never stored in plaintext.\n" +
|
||||
"\n" +
|
||||
" pgp - GNU Privacy Guard (GPG) key-based encryption \n" +
|
||||
" Uses your existing GPG key to encrypt/decrypt " +
|
||||
"the vault's master key.\n" +
|
||||
" Requires gpg to be installed and configured " +
|
||||
"with at least one secret key.\n" +
|
||||
" Use --keyid to specify a particular key, " +
|
||||
"otherwise uses your default GPG key."
|
||||
|
||||
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" {
|
||||
if runtime.GOOS == platformDarwin {
|
||||
supportedTypes = "passphrase, keychain, pgp, secure-enclave"
|
||||
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.
|
||||
|
||||
secure-enclave - Apple Secure Enclave hardware protection (macOS only)
|
||||
Stores the vault's master key encrypted by a non-exportable P-256 key
|
||||
held in the Secure Enclave. The key never leaves the hardware.
|
||||
Uses ECIES encryption; decryption is performed inside the SE.`
|
||||
typeDescriptions = "Available unlocker types:\n" +
|
||||
"\n" +
|
||||
" passphrase - Traditional password-based encryption\n" +
|
||||
" Prompts for a passphrase that will be " +
|
||||
"used to encrypt/decrypt the vault's master key.\n" +
|
||||
" The passphrase is never stored in " +
|
||||
"plaintext.\n" +
|
||||
"\n" +
|
||||
" keychain - macOS Keychain integration (macOS only)\n" +
|
||||
" Stores the vault's master key in the " +
|
||||
"macOS Keychain, protected by your login password.\n" +
|
||||
" Automatically unlocks when your Keychain " +
|
||||
"is unlocked (e.g., after login).\n" +
|
||||
" Provides seamless integration with macOS " +
|
||||
"security features like Touch ID.\n" +
|
||||
"\n" +
|
||||
" pgp - GNU Privacy Guard (GPG) key-based " +
|
||||
"encryption\n" +
|
||||
" Uses your existing GPG key to " +
|
||||
"encrypt/decrypt the vault's master key.\n" +
|
||||
" Requires gpg to be installed and " +
|
||||
"configured with at least one secret key.\n" +
|
||||
" Use --keyid to specify a particular key, " +
|
||||
"otherwise uses your default GPG key.\n" +
|
||||
"\n" +
|
||||
" secure-enclave - Apple Secure Enclave hardware protection " +
|
||||
"(macOS only)\n" +
|
||||
" Stores the vault's master key encrypted " +
|
||||
"by a non-exportable P-256 key\n" +
|
||||
" held in the Secure Enclave. The key " +
|
||||
"never leaves the hardware.\n" +
|
||||
" Uses ECIES encryption; decryption is " +
|
||||
"performed inside the SE."
|
||||
}
|
||||
|
||||
return supportedTypes, typeDescriptions
|
||||
}
|
||||
|
||||
func newUnlockerAddCmd() *cobra.Command {
|
||||
supportedTypes, typeDescriptions := unlockerAddHelp()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <type>",
|
||||
Short: "Add a new unlocker",
|
||||
Long: fmt.Sprintf(`Add a new unlocker to the current vault.
|
||||
|
||||
%s
|
||||
|
||||
Each vault can have multiple unlockers, allowing different authentication methods
|
||||
to access the same vault. This provides flexibility and backup access options.`, typeDescriptions),
|
||||
Long: "Add a new unlocker to the current vault.\n" +
|
||||
"\n" +
|
||||
typeDescriptions + "\n" +
|
||||
"\n" +
|
||||
"Each vault can have multiple unlockers, allowing different " +
|
||||
"authentication methods\n" +
|
||||
"to access the same vault. This provides flexibility and " +
|
||||
"backup access options.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgs: strings.Split(supportedTypes, ", "),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -166,33 +230,28 @@ to access the same vault. This provides flexibility and backup access options.`,
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize CLI: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
if !slices.Contains(validTypes, unlockerType) {
|
||||
return fmt.Errorf("%w '%s'\n\nSupported types: %s\n\n"+
|
||||
"Run 'secret unlocker add --help' for detailed descriptions",
|
||||
errInvalidUnlockerType, 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")
|
||||
if unlockerType != unlockerTypePGP && cmd.Flags().Changed("keyid") {
|
||||
return errKeyIDOnlyForPGP
|
||||
}
|
||||
|
||||
return cli.UnlockersAdd(unlockerType, cmd)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("keyid", "", "GPG key ID for PGP unlockers (optional, uses default key if not specified)")
|
||||
cmd.Flags().String("keyid", "",
|
||||
"GPG key ID for PGP unlockers (optional, uses default key if not specified)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -202,17 +261,20 @@ func newUnlockerRemoveCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize CLI: %v", err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove <unlocker-id>",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Remove an unlocker",
|
||||
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.`,
|
||||
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),
|
||||
ValidArgsFunction: getUnlockerIDsCompletionFunc(cli.fs, cli.stateDir),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
cli, err := NewCLIInstance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize CLI: %w", err)
|
||||
@@ -222,7 +284,8 @@ func newUnlockerRemoveCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("force", "f", false, "Force removal of last unlocker even if vault has secrets")
|
||||
cmd.Flags().BoolP("force", "f", false,
|
||||
"Force removal of last unlocker even if vault has secrets")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -249,6 +312,92 @@ func newUnlockerSelectCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// unlockerIDFromDir constructs an unlocker of the given metadata type
|
||||
// rooted at unlockerDir and returns its ID. Returns "" for unknown types
|
||||
// and, when includeSecureEnclave is false, for secure enclave unlockers.
|
||||
func unlockerIDFromDir(
|
||||
fs afero.Fs, unlockerDir string, metadata secret.UnlockerMetadata,
|
||||
includeSecureEnclave bool,
|
||||
) string {
|
||||
// Create the appropriate unlocker instance
|
||||
var unlocker secret.Unlocker
|
||||
|
||||
switch metadata.Type {
|
||||
case unlockerTypePassphrase:
|
||||
unlocker = secret.NewPassphraseUnlocker(fs, unlockerDir, metadata)
|
||||
case unlockerTypeKeychain:
|
||||
unlocker = secret.NewKeychainUnlocker(fs, unlockerDir, metadata)
|
||||
case unlockerTypePGP:
|
||||
unlocker = secret.NewPGPUnlocker(fs, unlockerDir, metadata)
|
||||
case unlockerTypeSecureEnclave:
|
||||
if includeSecureEnclave {
|
||||
unlocker = secret.NewSecureEnclaveUnlocker(fs, unlockerDir, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
if unlocker == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return unlocker.GetID()
|
||||
}
|
||||
|
||||
// findUnlockerIDByMetadata scans unlockersDir for the directory whose
|
||||
// stored metadata matches the given type and creation time and returns
|
||||
// the matching unlocker's ID. It returns ("", nil) when the directory is
|
||||
// readable but holds no match, and a non-nil error when the directory
|
||||
// itself cannot be read. Callers must distinguish the two: an unreadable
|
||||
// directory means the unlocker's real ID is unknowable, so the entry has
|
||||
// to be skipped rather than reported under a synthesized ID.
|
||||
func findUnlockerIDByMetadata(
|
||||
fs afero.Fs, unlockersDir string, metadata secret.UnlockerMetadata,
|
||||
includeSecureEnclave bool,
|
||||
) (string, error) {
|
||||
files, err := afero.ReadDir(fs, unlockersDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to read unlockers directory %s: %w", unlockersDir, err,
|
||||
)
|
||||
}
|
||||
|
||||
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(fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Could not read unlocker metadata file",
|
||||
"path", metadataPath, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var diskMetadata secret.UnlockerMetadata
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &diskMetadata)
|
||||
if err != nil {
|
||||
secret.Warn("Could not parse unlocker metadata file",
|
||||
"path", metadataPath, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Match by type and creation time
|
||||
if diskMetadata.Type == metadata.Type &&
|
||||
diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
||||
return unlockerIDFromDir(fs, unlockerDir, diskMetadata,
|
||||
includeSecureEnclave), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// UnlockersList lists unlockers in the current vault
|
||||
func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
// Get current vault
|
||||
@@ -259,6 +408,7 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
|
||||
// Get the current unlocker ID
|
||||
var currentUnlockerID string
|
||||
|
||||
currentUnlocker, err := vlt.GetCurrentUnlocker()
|
||||
if err == nil {
|
||||
currentUnlockerID = currentUnlocker.GetID()
|
||||
@@ -272,74 +422,40 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
|
||||
// Load actual unlocker objects to get the proper IDs
|
||||
var unlockers []UnlockerInfo
|
||||
|
||||
for _, metadata := range unlockerMetadataList {
|
||||
// Create unlocker instance to get the proper ID
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Warn("Could not get vault directory while listing unlockers", "error", err)
|
||||
secret.Warn("Could not get vault directory while listing unlockers",
|
||||
"error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the unlocker directory by type and created time
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
files, err := afero.ReadDir(cli.fs, unlockersDir)
|
||||
|
||||
unlockerID, err := findUnlockerIDByMetadata(
|
||||
cli.fs, unlockersDir, metadata, true,
|
||||
)
|
||||
if err != nil {
|
||||
secret.Warn("Could not read unlockers directory", "error", err)
|
||||
secret.Warn("Could not read unlockers directory, skipping unlocker",
|
||||
"unlockers_dir", unlockersDir, "error", err)
|
||||
|
||||
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 {
|
||||
secret.Warn("Could not read unlocker metadata file", "path", metadataPath, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var diskMetadata secret.UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
||||
secret.Warn("Could not parse unlocker metadata file", "path", metadataPath, "error", err)
|
||||
|
||||
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)
|
||||
case "secure-enclave":
|
||||
unlocker = secret.NewSecureEnclaveUnlocker(cli.fs, unlockerDir, diskMetadata)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get the proper ID using the unlocker's ID() method
|
||||
var properID string
|
||||
if unlocker != nil {
|
||||
properID = unlocker.GetID()
|
||||
if unlockerID != "" {
|
||||
properID = unlockerID
|
||||
} else {
|
||||
// Generate ID as fallback
|
||||
properID = fmt.Sprintf("%s-%s", metadata.CreatedAt.Format("2006-01-02.15.04"), metadata.Type)
|
||||
secret.Warn("Could not create unlocker instance, using fallback ID", "fallback_id", properID, "type", metadata.Type)
|
||||
properID = fmt.Sprintf("%s-%s",
|
||||
metadata.CreatedAt.Format("2006-01-02.15.04"), metadata.Type)
|
||||
secret.Warn("Could not create unlocker instance, using fallback ID",
|
||||
"fallback_id", properID, "type", metadata.Type)
|
||||
}
|
||||
|
||||
unlockerInfo := UnlockerInfo{
|
||||
@@ -360,8 +476,10 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
||||
}
|
||||
|
||||
// printUnlockersJSON prints unlockers in JSON format
|
||||
func (cli *Instance) printUnlockersJSON(unlockers []UnlockerInfo, currentUnlockerID string) error {
|
||||
output := map[string]interface{}{
|
||||
func (cli *Instance) printUnlockersJSON(
|
||||
unlockers []UnlockerInfo, currentUnlockerID string,
|
||||
) error {
|
||||
output := map[string]any{
|
||||
"unlockers": unlockers,
|
||||
"currentUnlockerID": currentUnlockerID,
|
||||
}
|
||||
@@ -395,10 +513,12 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
||||
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,
|
||||
@@ -414,164 +534,186 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
||||
|
||||
// UnlockersAdd adds a new unlocker
|
||||
func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
|
||||
// Build the supported types list based on platform
|
||||
supportedTypes := "passphrase, pgp"
|
||||
if runtime.GOOS == "darwin" {
|
||||
supportedTypes = "passphrase, keychain, pgp, secure-enclave"
|
||||
}
|
||||
|
||||
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 passphraseBuffer *memguard.LockedBuffer
|
||||
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
|
||||
passphraseBuffer = memguard.NewBufferFromBytes([]byte(envPassphrase))
|
||||
} else {
|
||||
// Use secure passphrase input with confirmation
|
||||
passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
}
|
||||
defer passphraseBuffer.Destroy()
|
||||
|
||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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":
|
||||
if runtime.GOOS != "darwin" {
|
||||
return fmt.Errorf("keychain unlockers are only supported on macOS")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 "secure-enclave":
|
||||
if runtime.GOOS != "darwin" {
|
||||
return fmt.Errorf("secure enclave unlockers are only supported on macOS")
|
||||
}
|
||||
|
||||
seUnlocker, err := secret.CreateSecureEnclaveUnlocker(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Secure Enclave unlocker: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Created Secure Enclave unlocker: %s\n", seUnlocker.GetID())
|
||||
|
||||
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(seUnlocker.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":
|
||||
// Get GPG key ID from flag, environment, or default key
|
||||
var gpgKeyID string
|
||||
if flagKeyID, _ := cmd.Flags().GetString("keyid"); flagKeyID != "" {
|
||||
gpgKeyID = flagKeyID
|
||||
} else if envKeyID := os.Getenv(secret.EnvGPGKeyID); envKeyID != "" {
|
||||
gpgKeyID = envKeyID
|
||||
} else {
|
||||
// Try to get the default GPG key
|
||||
defaultKeyID, err := getDefaultGPGKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no GPG key specified and no default key found: %w", err)
|
||||
}
|
||||
gpgKeyID = defaultKeyID
|
||||
cmd.Printf("Using default GPG key: %s\n", gpgKeyID)
|
||||
}
|
||||
|
||||
// Check if this key is already added as an unlocker
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the GPG key ID to its fingerprint
|
||||
fingerprint, err := secret.ResolveGPGKeyFingerprint(gpgKeyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
||||
}
|
||||
|
||||
// Check if this GPG key is already added
|
||||
expectedID := fmt.Sprintf("pgp-%s", fingerprint)
|
||||
if err := cli.checkUnlockerExists(vlt, expectedID); err != nil {
|
||||
return fmt.Errorf("GPG key %s is already added as an unlocker", gpgKeyID)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
case unlockerTypePassphrase:
|
||||
return cli.addPassphraseUnlocker(cmd)
|
||||
case unlockerTypeKeychain:
|
||||
return cli.addKeychainUnlocker(cmd)
|
||||
case unlockerTypeSecureEnclave:
|
||||
return cli.addSecureEnclaveUnlocker(cmd)
|
||||
case unlockerTypePGP:
|
||||
return cli.addPGPUnlocker(cmd)
|
||||
default:
|
||||
return fmt.Errorf("unsupported unlocker type: %s (supported: %s)", unlockerType, supportedTypes)
|
||||
// Build the supported types list based on platform
|
||||
supportedTypes := "passphrase, pgp"
|
||||
if runtime.GOOS == platformDarwin {
|
||||
supportedTypes = "passphrase, keychain, pgp, secure-enclave"
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s (supported: %s)",
|
||||
errUnsupportedUnlockerType, unlockerType, supportedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// autoSelectUnlocker selects the newly created unlocker as current,
|
||||
// printing a warning if selection fails
|
||||
func autoSelectUnlocker(cmd *cobra.Command, vlt *vault.Vault, unlockerID string) {
|
||||
err := vlt.SelectUnlocker(unlockerID)
|
||||
if err != nil {
|
||||
cmd.Printf("Warning: Failed to auto-select new unlocker: %v\n", err)
|
||||
} else {
|
||||
cmd.Printf("Automatically selected as current unlocker\n")
|
||||
}
|
||||
}
|
||||
|
||||
// addPassphraseUnlocker creates a passphrase unlocker in the current vault
|
||||
func (cli *Instance) addPassphraseUnlocker(cmd *cobra.Command) error {
|
||||
// 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 passphraseBuffer *memguard.LockedBuffer
|
||||
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
|
||||
passphraseBuffer = memguard.NewBufferFromBytes([]byte(envPassphrase))
|
||||
} else {
|
||||
// Use secure passphrase input with confirmation
|
||||
passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
}
|
||||
defer passphraseBuffer.Destroy()
|
||||
|
||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
|
||||
|
||||
// Auto-select the newly created unlocker
|
||||
autoSelectUnlocker(cmd, vlt, passphraseUnlocker.GetID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addKeychainUnlocker creates a macOS Keychain unlocker in the current vault
|
||||
func (cli *Instance) addKeychainUnlocker(cmd *cobra.Command) error {
|
||||
if runtime.GOOS != platformDarwin {
|
||||
return errKeychainMacOSOnly
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
keyName, err := keychainUnlocker.GetKeychainItemName()
|
||||
if err == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
autoSelectUnlocker(cmd, vlt, keychainUnlocker.GetID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addSecureEnclaveUnlocker creates a Secure Enclave unlocker in the
|
||||
// current vault
|
||||
func (cli *Instance) addSecureEnclaveUnlocker(cmd *cobra.Command) error {
|
||||
if runtime.GOOS != platformDarwin {
|
||||
return errSecureEnclaveMacOSOnly
|
||||
}
|
||||
|
||||
seUnlocker, err := secret.CreateSecureEnclaveUnlocker(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Secure Enclave unlocker: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Created Secure Enclave unlocker: %s\n", seUnlocker.GetID())
|
||||
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
autoSelectUnlocker(cmd, vlt, seUnlocker.GetID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addPGPUnlocker creates a PGP unlocker in the current vault
|
||||
func (cli *Instance) addPGPUnlocker(cmd *cobra.Command) error {
|
||||
// Get GPG key ID from flag, environment, or default key
|
||||
var gpgKeyID string
|
||||
if flagKeyID, _ := cmd.Flags().GetString("keyid"); flagKeyID != "" {
|
||||
gpgKeyID = flagKeyID
|
||||
} else if envKeyID := os.Getenv(secret.EnvGPGKeyID); envKeyID != "" {
|
||||
gpgKeyID = envKeyID
|
||||
} else {
|
||||
// Try to get the default GPG key
|
||||
defaultKeyID, err := getDefaultGPGKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no GPG key specified and no default key found: %w", err)
|
||||
}
|
||||
|
||||
gpgKeyID = defaultKeyID
|
||||
cmd.Printf("Using default GPG key: %s\n", gpgKeyID)
|
||||
}
|
||||
|
||||
// Check if this key is already added as an unlocker
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the GPG key ID to its fingerprint
|
||||
fingerprint, err := secret.ResolveGPGKeyFingerprint(gpgKeyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
||||
}
|
||||
|
||||
// Check if this GPG key is already added
|
||||
expectedID := "pgp-" + fingerprint
|
||||
|
||||
err = cli.checkUnlockerExists(vlt, expectedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GPG key %s %w", gpgKeyID, errGPGKeyAlreadyUnlocker)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Auto-select the newly created unlocker
|
||||
autoSelectUnlocker(cmd, vlt, pgpUnlocker.GetID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlockersRemove removes an unlocker with safety checks
|
||||
func (cli *Instance) UnlockersRemove(unlockerID string, force bool, cmd *cobra.Command) error {
|
||||
func (cli *Instance) UnlockersRemove(
|
||||
unlockerID string, force bool, cmd *cobra.Command,
|
||||
) error {
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
@@ -593,20 +735,24 @@ func (cli *Instance) UnlockersRemove(unlockerID string, force bool, cmd *cobra.C
|
||||
}
|
||||
|
||||
if numSecrets > 0 && !force {
|
||||
cmd.Println("ERROR: Cannot remove the last unlocker when the vault contains secrets.")
|
||||
cmd.Println("WARNING: Without unlockers, you MUST have your mnemonic phrase to decrypt the vault.")
|
||||
cmd.Println("ERROR: Cannot remove the last unlocker when the " +
|
||||
"vault contains secrets.")
|
||||
cmd.Println("WARNING: Without unlockers, you MUST have your " +
|
||||
"mnemonic phrase to decrypt the vault.")
|
||||
cmd.Println("If you want to proceed anyway, use --force")
|
||||
|
||||
return fmt.Errorf("refusing to remove last unlocker")
|
||||
return errLastUnlocker
|
||||
}
|
||||
|
||||
if numSecrets > 0 && force {
|
||||
cmd.Println("WARNING: Removing the last unlocker. You MUST have your mnemonic phrase to access this vault again!")
|
||||
cmd.Println("WARNING: Removing the last unlocker. You MUST " +
|
||||
"have your mnemonic phrase to access this vault again!")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the unlocker
|
||||
if err := vlt.RemoveUnlocker(unlockerID); err != nil {
|
||||
err = vlt.RemoveUnlocker(unlockerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -639,65 +785,29 @@ func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) er
|
||||
// Get vault directory to construct unlocker instances
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Warn("Could not get vault directory during duplicate check", "error", err)
|
||||
secret.Warn("Could not get vault directory during duplicate check",
|
||||
"error", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check each unlocker's ID
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
for _, metadata := range unlockers {
|
||||
// Construct the unlocker based on type to get its ID
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
files, err := afero.ReadDir(cli.fs, unlockersDir)
|
||||
// Construct the unlocker matching this metadata to get its ID
|
||||
id, err := findUnlockerIDByMetadata(cli.fs, unlockersDir, metadata, true)
|
||||
if err != nil {
|
||||
secret.Warn("Could not read unlockers directory during duplicate check", "error", err)
|
||||
secret.Warn(
|
||||
"Could not read unlockers directory during duplicate check, "+
|
||||
"skipping unlocker",
|
||||
"unlockers_dir", unlockersDir, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
unlockerDir := filepath.Join(unlockersDir, file.Name())
|
||||
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||
|
||||
// Check if this matches our metadata
|
||||
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Could not read unlocker metadata during duplicate check", "path", metadataPath, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var diskMetadata secret.UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
||||
secret.Warn("Could not parse unlocker metadata during duplicate check", "path", metadataPath, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Match by type and creation time
|
||||
if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
||||
var unlocker secret.Unlocker
|
||||
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)
|
||||
case "secure-enclave":
|
||||
unlocker = secret.NewSecureEnclaveUnlocker(cli.fs, unlockerDir, diskMetadata)
|
||||
}
|
||||
|
||||
if unlocker != nil && unlocker.GetID() == unlockerID {
|
||||
return fmt.Errorf("unlocker already exists")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
if id != "" && id == unlockerID {
|
||||
return errUnlockerExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user