Compare commits
1 Commits
397011a592
...
9ee216f629
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ee216f629 |
8
TODO.md
8
TODO.md
@@ -32,13 +32,7 @@ Bring the repo into policy compliance in one commit:
|
|||||||
findings across `internal/` and `pkg/` (line wrapping, `wsl_v5`
|
findings across `internal/` and `pkg/` (line wrapping, `wsl_v5`
|
||||||
blank lines, sentinel errors for `err113`, `t.Parallel()` where
|
blank lines, sentinel errors for `err113`, `t.Parallel()` where
|
||||||
safe, `_test` package conversions, complexity/`dupl` helper
|
safe, `_test` package conversions, complexity/`dupl` helper
|
||||||
extraction) on branch `golangci-v2.12.2`. Reworked after review:
|
extraction) on branch `golangci-v2.12.2`.
|
||||||
the `err113` sentinels in `internal/vault`, `internal/secret`,
|
|
||||||
`internal/cli` and `pkg/bip85` were reshaped so every composed
|
|
||||||
error message is byte-identical to `main`, and
|
|
||||||
`findUnlockerIDByMetadata` now returns an error so `unlocker list`
|
|
||||||
skips an unreadable `unlockers.d` entry with a warning instead of
|
|
||||||
emitting a fabricated fallback ID.
|
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
||||||
Makefile shims, README Entrypoints section
|
Makefile shims, README Entrypoints section
|
||||||
- 2026-03-11: Secure Enclave unlocker for hardware-backed secret
|
- 2026-03-11: Secure Enclave unlocker for hardware-backed secret
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
|
||||||
"git.eeqj.de/sneak/secret/internal/vault"
|
"git.eeqj.de/sneak/secret/internal/vault"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -76,18 +75,7 @@ func getUnlockerIDsCompletionFunc(fs afero.Fs, stateDir string) func(
|
|||||||
|
|
||||||
for _, metadata := range unlockerMetadataList {
|
for _, metadata := range unlockerMetadataList {
|
||||||
// Get the actual unlocker ID by creating the unlocker instance
|
// Get the actual unlocker ID by creating the unlocker instance
|
||||||
id, err := findUnlockerIDByMetadata(
|
id := findUnlockerIDByMetadata(fs, unlockersDir, metadata, false)
|
||||||
fs, unlockersDir, metadata, false,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
secret.Warn(
|
|
||||||
"Could not read unlockers directory during completion, "+
|
|
||||||
"skipping unlocker",
|
|
||||||
"unlockers_dir", unlockersDir, "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != "" && strings.HasPrefix(id, toComplete) {
|
if id != "" && strings.HasPrefix(id, toComplete) {
|
||||||
completions = append(completions, id)
|
completions = append(completions, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,8 @@ var (
|
|||||||
"keychain unlockers are only supported on macOS")
|
"keychain unlockers are only supported on macOS")
|
||||||
errSecureEnclaveMacOSOnly = errors.New(
|
errSecureEnclaveMacOSOnly = errors.New(
|
||||||
"secure enclave unlockers are only supported on macOS")
|
"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(
|
errGPGKeyAlreadyUnlocker = errors.New(
|
||||||
"is already added as an unlocker")
|
"GPG key is already added as an unlocker")
|
||||||
errUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
errUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
||||||
errLastUnlocker = errors.New("refusing to remove last unlocker")
|
errLastUnlocker = errors.New("refusing to remove last unlocker")
|
||||||
errUnlockerExists = errors.New("unlocker already exists")
|
errUnlockerExists = errors.New("unlocker already exists")
|
||||||
@@ -344,20 +342,16 @@ func unlockerIDFromDir(
|
|||||||
|
|
||||||
// findUnlockerIDByMetadata scans unlockersDir for the directory whose
|
// findUnlockerIDByMetadata scans unlockersDir for the directory whose
|
||||||
// stored metadata matches the given type and creation time and returns
|
// stored metadata matches the given type and creation time and returns
|
||||||
// the matching unlocker's ID. It returns ("", nil) when the directory is
|
// the matching unlocker's ID. Returns "" if no match is found.
|
||||||
// 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(
|
func findUnlockerIDByMetadata(
|
||||||
fs afero.Fs, unlockersDir string, metadata secret.UnlockerMetadata,
|
fs afero.Fs, unlockersDir string, metadata secret.UnlockerMetadata,
|
||||||
includeSecureEnclave bool,
|
includeSecureEnclave bool,
|
||||||
) (string, error) {
|
) string {
|
||||||
files, err := afero.ReadDir(fs, unlockersDir)
|
files, err := afero.ReadDir(fs, unlockersDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf(
|
secret.Warn("Could not read unlockers directory", "error", err)
|
||||||
"failed to read unlockers directory %s: %w", unlockersDir, err,
|
|
||||||
)
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -391,11 +385,11 @@ func findUnlockerIDByMetadata(
|
|||||||
if diskMetadata.Type == metadata.Type &&
|
if diskMetadata.Type == metadata.Type &&
|
||||||
diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
||||||
return unlockerIDFromDir(fs, unlockerDir, diskMetadata,
|
return unlockerIDFromDir(fs, unlockerDir, diskMetadata,
|
||||||
includeSecureEnclave), nil
|
includeSecureEnclave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnlockersList lists unlockers in the current vault
|
// UnlockersList lists unlockers in the current vault
|
||||||
@@ -435,16 +429,7 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error {
|
|||||||
|
|
||||||
// Find the unlocker directory by type and created time
|
// Find the unlocker directory by type and created time
|
||||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
|
unlockerID := findUnlockerIDByMetadata(cli.fs, unlockersDir, metadata, true)
|
||||||
unlockerID, err := findUnlockerIDByMetadata(
|
|
||||||
cli.fs, unlockersDir, metadata, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
secret.Warn("Could not read unlockers directory, skipping unlocker",
|
|
||||||
"unlockers_dir", unlockersDir, "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the proper ID using the unlocker's ID() method
|
// Get the proper ID using the unlocker's ID() method
|
||||||
var properID string
|
var properID string
|
||||||
@@ -693,7 +678,7 @@ func (cli *Instance) addPGPUnlocker(cmd *cobra.Command) error {
|
|||||||
|
|
||||||
err = cli.checkUnlockerExists(vlt, expectedID)
|
err = cli.checkUnlockerExists(vlt, expectedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GPG key %s %w", gpgKeyID, errGPGKeyAlreadyUnlocker)
|
return fmt.Errorf("%w: %s", errGPGKeyAlreadyUnlocker, gpgKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir, gpgKeyID)
|
pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir, gpgKeyID)
|
||||||
@@ -796,16 +781,7 @@ func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) er
|
|||||||
|
|
||||||
for _, metadata := range unlockers {
|
for _, metadata := range unlockers {
|
||||||
// Construct the unlocker matching this metadata to get its ID
|
// Construct the unlocker matching this metadata to get its ID
|
||||||
id, err := findUnlockerIDByMetadata(cli.fs, unlockersDir, metadata, true)
|
id := findUnlockerIDByMetadata(cli.fs, unlockersDir, metadata, true)
|
||||||
if err != nil {
|
|
||||||
secret.Warn(
|
|
||||||
"Could not read unlockers directory during duplicate check, "+
|
|
||||||
"skipping unlocker",
|
|
||||||
"unlockers_dir", unlockersDir, "error", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != "" && id == unlockerID {
|
if id != "" && id == unlockerID {
|
||||||
return errUnlockerExists
|
return errUnlockerExists
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
// Unlocker List Tests
|
|
||||||
//
|
|
||||||
// Tests for `secret unlocker list` behavior when the unlockers.d directory
|
|
||||||
// cannot be read while the listing is being rendered:
|
|
||||||
//
|
|
||||||
// - TestUnlockersListSkipsUnreadableUnlockersDir: an unreadable
|
|
||||||
// unlockers.d yields no rows rather than rows bearing synthesized IDs.
|
|
||||||
// - TestUnlockersListSkipsOnlyUnreadableEntries: a readable entry is
|
|
||||||
// still listed, with its real ID and its current-unlocker marker,
|
|
||||||
// when a later entry's scan fails.
|
|
||||||
//
|
|
||||||
// The listing resolves each unlocker's real ID by rescanning unlockers.d
|
|
||||||
// after the vault has already enumerated it. If that rescan fails the ID
|
|
||||||
// is unknowable, so the entry must be skipped: a synthesized ID matches
|
|
||||||
// no `unlocker remove` or `unlocker select` argument and would also
|
|
||||||
// suppress the current-unlocker marker.
|
|
||||||
|
|
||||||
//nolint:testpackage // white-box test of unexported internals
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// listTestStateDir is the state directory of the synthetic vault used
|
|
||||||
// by the unlocker listing tests.
|
|
||||||
listTestStateDir = "/state"
|
|
||||||
|
|
||||||
// listTestVaultName is the name of that synthetic vault.
|
|
||||||
listTestVaultName = "default"
|
|
||||||
|
|
||||||
// listTestGPGKeyID is the GPG key ID recorded in the readable PGP
|
|
||||||
// unlocker's metadata. The unlocker's real ID is derived from it, and
|
|
||||||
// differs from the timestamp-derived fallback ID.
|
|
||||||
listTestGPGKeyID = "DEADBEEFDEADBEEF"
|
|
||||||
|
|
||||||
// listTestUnlockerDirOne and listTestUnlockerDirTwo are the unlocker
|
|
||||||
// directory names under unlockers.d.
|
|
||||||
listTestUnlockerDirOne = "host-pgp-2026-08-09"
|
|
||||||
listTestUnlockerDirTwo = "host-pgp-2026-08-10"
|
|
||||||
|
|
||||||
// listTestUnlockersDirName is the directory the listing rescans to
|
|
||||||
// resolve unlocker IDs.
|
|
||||||
listTestUnlockersDirName = "unlockers.d"
|
|
||||||
|
|
||||||
// listTestMetadataFileName is the per-unlocker metadata file name.
|
|
||||||
listTestMetadataFileName = "unlocker-metadata.json"
|
|
||||||
|
|
||||||
// listTestDirPerm and listTestFilePerm are the fixture permissions.
|
|
||||||
listTestDirPerm = 0o700
|
|
||||||
listTestFilePerm = 0o600
|
|
||||||
)
|
|
||||||
|
|
||||||
// errUnlockersDirUnreadable is returned by the test filesystem in place of
|
|
||||||
// a successful open of unlockers.d.
|
|
||||||
var errUnlockersDirUnreadable = errors.New("permission denied")
|
|
||||||
|
|
||||||
// unlockersDirFailFs makes unlockers.d unreadable once it has been opened
|
|
||||||
// successfully openBudget times. This reproduces the directory becoming
|
|
||||||
// unreadable (permission change, partially restored backup, EIO) between
|
|
||||||
// the vault's own enumeration and the per-entry rescan that resolves
|
|
||||||
// unlocker IDs.
|
|
||||||
type unlockersDirFailFs struct {
|
|
||||||
afero.Fs
|
|
||||||
|
|
||||||
openBudget int
|
|
||||||
opens int
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:ireturn // afero.File is the interface required by afero.Fs
|
|
||||||
func (f *unlockersDirFailFs) Open(name string) (afero.File, error) {
|
|
||||||
if filepath.Base(name) == listTestUnlockersDirName {
|
|
||||||
f.opens++
|
|
||||||
if f.opens > f.openBudget {
|
|
||||||
return nil, errUnlockersDirUnreadable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:wrapcheck // test double must return the wrapped Fs error as-is
|
|
||||||
return f.Fs.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writePGPUnlocker writes a PGP unlocker directory with metadata that
|
|
||||||
// yields the real ID "pgp-<keyID>".
|
|
||||||
func writePGPUnlocker(
|
|
||||||
t *testing.T, fs afero.Fs, unlockersDir, dirName string,
|
|
||||||
createdAt time.Time, keyID string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
metadata := secret.PGPUnlockerMetadata{
|
|
||||||
UnlockerMetadata: secret.UnlockerMetadata{
|
|
||||||
Type: unlockerTypePGP,
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
},
|
|
||||||
GPGKeyID: keyID,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := json.Marshal(metadata)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
dir := filepath.Join(unlockersDir, dirName)
|
|
||||||
require.NoError(t, fs.MkdirAll(dir, listTestDirPerm))
|
|
||||||
require.NoError(t, afero.WriteFile(
|
|
||||||
fs, filepath.Join(dir, listTestMetadataFileName), encoded,
|
|
||||||
listTestFilePerm,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// newListTestVault builds a synthetic vault on a MemMapFs containing the
|
|
||||||
// given number of PGP unlockers, with the first one selected as current.
|
|
||||||
func newListTestVault(t *testing.T, unlockerCount int) *afero.MemMapFs {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
base := &afero.MemMapFs{}
|
|
||||||
vaultDir := filepath.Join(listTestStateDir, "vaults.d", listTestVaultName)
|
|
||||||
unlockersDir := filepath.Join(vaultDir, listTestUnlockersDirName)
|
|
||||||
|
|
||||||
require.NoError(t, afero.WriteFile(
|
|
||||||
base, filepath.Join(listTestStateDir, "currentvault"),
|
|
||||||
[]byte(listTestVaultName), listTestFilePerm,
|
|
||||||
))
|
|
||||||
|
|
||||||
names := []string{listTestUnlockerDirOne, listTestUnlockerDirTwo}
|
|
||||||
names = names[:unlockerCount]
|
|
||||||
|
|
||||||
for i, name := range names {
|
|
||||||
writePGPUnlocker(t, base, unlockersDir, name,
|
|
||||||
time.Date(2026, time.August, 9+i, 12, 30, 0, 0, time.UTC),
|
|
||||||
listTestGPGKeyID+string(rune('A'+i)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, afero.WriteFile(
|
|
||||||
base, filepath.Join(vaultDir, "current-unlocker"),
|
|
||||||
[]byte(names[0]), listTestFilePerm,
|
|
||||||
))
|
|
||||||
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
// listUnlockersJSON runs UnlockersList in JSON mode against the given
|
|
||||||
// filesystem and decodes the emitted unlocker rows.
|
|
||||||
func listUnlockersJSON(t *testing.T, fs afero.Fs) []UnlockerInfo {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
cmd := &cobra.Command{}
|
|
||||||
cmd.SetOut(&buf)
|
|
||||||
cmd.SetErr(&buf)
|
|
||||||
|
|
||||||
instance := &Instance{fs: fs, stateDir: listTestStateDir, cmd: cmd}
|
|
||||||
require.NoError(t, instance.UnlockersList(true))
|
|
||||||
|
|
||||||
var decoded struct {
|
|
||||||
Unlockers []UnlockerInfo `json:"unlockers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &decoded))
|
|
||||||
|
|
||||||
return decoded.Unlockers
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUnlockersListSkipsUnreadableUnlockersDir asserts that an unlockers.d
|
|
||||||
// which becomes unreadable after the vault enumerated it produces no rows,
|
|
||||||
// rather than rows carrying fabricated fallback IDs.
|
|
||||||
func TestUnlockersListSkipsUnreadableUnlockersDir(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
base := newListTestVault(t, 1)
|
|
||||||
// Budget of one: the vault's own ListUnlockers scan succeeds, the
|
|
||||||
// per-entry rescan that resolves the ID fails.
|
|
||||||
fs := &unlockersDirFailFs{Fs: base, openBudget: 1}
|
|
||||||
|
|
||||||
unlockers := listUnlockersJSON(t, fs)
|
|
||||||
|
|
||||||
assert.Empty(t, unlockers,
|
|
||||||
"an unreadable unlockers.d must yield no rows, not fabricated IDs")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUnlockersListSkipsOnlyUnreadableEntries asserts that a readable
|
|
||||||
// entry survives with its real ID and current-unlocker marker when a later
|
|
||||||
// entry's rescan fails.
|
|
||||||
func TestUnlockersListSkipsOnlyUnreadableEntries(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
base := newListTestVault(t, 2)
|
|
||||||
// Budget of two: ListUnlockers plus the first entry's rescan succeed,
|
|
||||||
// the second entry's rescan fails.
|
|
||||||
fs := &unlockersDirFailFs{Fs: base, openBudget: 2}
|
|
||||||
|
|
||||||
unlockers := listUnlockersJSON(t, fs)
|
|
||||||
|
|
||||||
require.Len(t, unlockers, 1,
|
|
||||||
"only the entry whose directory was readable may be listed")
|
|
||||||
assert.Equal(t, "pgp-"+listTestGPGKeyID+"A", unlockers[0].ID,
|
|
||||||
"the surviving row must carry the real unlocker ID")
|
|
||||||
assert.True(t, unlockers[0].IsCurrent,
|
|
||||||
"the current-unlocker marker must survive the skip")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUnlockersListReadableEntriesAreListed is the control case: with a
|
|
||||||
// fully readable unlockers.d every entry is listed with its real ID.
|
|
||||||
func TestUnlockersListReadableEntriesAreListed(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
base := newListTestVault(t, 2)
|
|
||||||
|
|
||||||
unlockers := listUnlockersJSON(t, base)
|
|
||||||
|
|
||||||
require.Len(t, unlockers, 2)
|
|
||||||
assert.Equal(t, "pgp-"+listTestGPGKeyID+"A", unlockers[0].ID)
|
|
||||||
assert.Equal(t, "pgp-"+listTestGPGKeyID+"B", unlockers[1].ID)
|
|
||||||
assert.True(t, unlockers[0].IsCurrent)
|
|
||||||
assert.False(t, unlockers[1].IsCurrent)
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// errSecretNotFound carries only the message tail; callers compose
|
errSecretNotFound = errors.New("secret not found")
|
||||||
// "secret <name> not found" around it so the emitted text is
|
|
||||||
// unchanged.
|
|
||||||
errSecretNotFound = errors.New("not found")
|
|
||||||
errUnlockerRequired = errors.New("unlocker required to decrypt secret")
|
errUnlockerRequired = errors.New("unlocker required to decrypt secret")
|
||||||
errGetEncryptedDataDeprecated = errors.New(
|
errGetEncryptedDataDeprecated = errors.New(
|
||||||
"GetEncryptedData is deprecated - use version-specific methods")
|
"GetEncryptedData is deprecated - use version-specific methods")
|
||||||
@@ -97,7 +94,7 @@ func (s *Secret) GetValue(unlocker Unlocker) (*memguard.LockedBuffer, error) {
|
|||||||
Debug("Secret not found during GetValue",
|
Debug("Secret not found during GetValue",
|
||||||
"secret_name", s.Name, "vault_name", s.vault.GetName())
|
"secret_name", s.Name, "vault_name", s.vault.GetName())
|
||||||
|
|
||||||
return nil, fmt.Errorf("secret %s %w", s.Name, errSecretNotFound)
|
return nil, fmt.Errorf("%w: %s", errSecretNotFound, s.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug("Secret exists, getting current version", "secret_name", s.Name)
|
Debug("Secret exists, getting current version", "secret_name", s.Name)
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ package vault
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// Sentinel errors returned by vault operations.
|
// Sentinel errors returned by vault operations.
|
||||||
//
|
|
||||||
// Several of these carry deliberately partial text: the message a caller
|
|
||||||
// composes with fmt.Errorf places the interpolated value where it has
|
|
||||||
// always appeared, and the sentinel supplies only the surrounding fixed
|
|
||||||
// words. This keeps every composed message byte-identical to the dynamic
|
|
||||||
// errors these sentinels replaced. Each such sentinel notes the message it
|
|
||||||
// participates in.
|
|
||||||
var (
|
var (
|
||||||
// ErrMnemonicMismatch indicates the mnemonic-derived public key does
|
// ErrMnemonicMismatch indicates the mnemonic-derived public key does
|
||||||
// not match the vault's stored public key hash.
|
// not match the vault's stored public key hash.
|
||||||
@@ -18,48 +11,43 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidVaultName indicates a vault name that does not match the
|
// ErrInvalidVaultName indicates a vault name that does not match the
|
||||||
// allowed pattern [a-z0-9.\-_]+. Composed as
|
// allowed pattern [a-z0-9.\-_]+.
|
||||||
// "invalid vault name '<name>': must match pattern [a-z0-9.\-_]+".
|
ErrInvalidVaultName = errors.New(
|
||||||
ErrInvalidVaultName = errors.New("invalid vault name")
|
"invalid vault name: must match pattern [a-z0-9.\\-_]+",
|
||||||
|
)
|
||||||
|
|
||||||
// ErrVaultNotFound indicates the named vault does not exist. Composed
|
// ErrVaultNotFound indicates the named vault does not exist.
|
||||||
// as "vault <name> does not exist".
|
ErrVaultNotFound = errors.New("vault does not exist")
|
||||||
ErrVaultNotFound = errors.New("does not exist")
|
|
||||||
|
|
||||||
// ErrNilValueBuffer indicates a nil value buffer was supplied.
|
// ErrNilValueBuffer indicates a nil value buffer was supplied.
|
||||||
ErrNilValueBuffer = errors.New("value buffer is nil")
|
ErrNilValueBuffer = errors.New("value buffer is nil")
|
||||||
|
|
||||||
// ErrInvalidSecretName indicates a secret name that does not match
|
// ErrInvalidSecretName indicates a secret name that does not match
|
||||||
// the allowed pattern [a-z0-9.\-_/]+. Composed as
|
// the allowed pattern [a-z0-9.\-_/]+.
|
||||||
// "invalid secret name '<name>': must match pattern [a-z0-9.\-_/]+",
|
ErrInvalidSecretName = errors.New(
|
||||||
// or as "invalid secret name: <name>" by GetSecretObject.
|
"invalid secret name: must match pattern [a-z0-9.\\-_/]+",
|
||||||
ErrInvalidSecretName = errors.New("invalid secret name")
|
)
|
||||||
|
|
||||||
// ErrSecretExists indicates the secret already exists and --force
|
// ErrSecretExists indicates the secret already exists and --force
|
||||||
// was not supplied. Composed as
|
// was not supplied.
|
||||||
// "secret <name> already exists (use --force to overwrite)", or as
|
ErrSecretExists = errors.New(
|
||||||
// "secret '<name>' already exists in vault '<vault>' (use --force to
|
"secret already exists (use --force to overwrite)",
|
||||||
// overwrite)" when copying between vaults.
|
)
|
||||||
ErrSecretExists = errors.New("already exists")
|
|
||||||
|
|
||||||
// ErrSecretNotFound indicates the named secret does not exist.
|
// ErrSecretNotFound indicates the named secret does not exist.
|
||||||
// Composed as "secret <name> not found".
|
ErrSecretNotFound = errors.New("secret not found")
|
||||||
ErrSecretNotFound = errors.New("not found")
|
|
||||||
|
|
||||||
// ErrVersionNotFound indicates the requested secret version does not
|
// ErrVersionNotFound indicates the requested secret version does not
|
||||||
// exist. Composed as
|
// exist.
|
||||||
// "version <version> not found for secret <name>".
|
ErrVersionNotFound = errors.New("version not found")
|
||||||
ErrVersionNotFound = errors.New("not found for secret")
|
|
||||||
|
|
||||||
// ErrNoVersions indicates the source secret has no versions. Composed
|
// ErrNoVersions indicates the source secret has no versions.
|
||||||
// as "source secret '<name>' has no versions".
|
ErrNoVersions = errors.New("source secret has no versions")
|
||||||
ErrNoVersions = errors.New("has no versions")
|
|
||||||
|
|
||||||
// ErrUnsupportedUnlockerType indicates an unlocker metadata type
|
// ErrUnsupportedUnlockerType indicates an unlocker metadata type
|
||||||
// that this build does not support.
|
// that this build does not support.
|
||||||
ErrUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
ErrUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
||||||
|
|
||||||
// ErrUnlockerNotFound indicates no unlocker with the given ID exists.
|
// ErrUnlockerNotFound indicates no unlocker with the given ID exists.
|
||||||
// Composed as "unlocker with ID <id> not found".
|
ErrUnlockerNotFound = errors.New("unlocker not found")
|
||||||
ErrUnlockerNotFound = errors.New("not found")
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -199,10 +199,7 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
|||||||
if !isValidVaultName(name) {
|
if !isValidVaultName(name) {
|
||||||
secret.Debug("Invalid vault name provided", "vault_name", name)
|
secret.Debug("Invalid vault name provided", "vault_name", name)
|
||||||
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("%w: '%s'", ErrInvalidVaultName, name)
|
||||||
"%w '%s': must match pattern [a-z0-9.\\-_]+",
|
|
||||||
ErrInvalidVaultName, name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.Debug("Vault name validation passed", "vault_name", name)
|
secret.Debug("Vault name validation passed", "vault_name", name)
|
||||||
@@ -275,10 +272,7 @@ func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
|||||||
if !isValidVaultName(name) {
|
if !isValidVaultName(name) {
|
||||||
secret.Debug("Invalid vault name provided", "vault_name", name)
|
secret.Debug("Invalid vault name provided", "vault_name", name)
|
||||||
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("%w: '%s'", ErrInvalidVaultName, name)
|
||||||
"%w '%s': must match pattern [a-z0-9.\\-_]+",
|
|
||||||
ErrInvalidVaultName, name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.Debug("Vault name validation passed", "vault_name", name)
|
secret.Debug("Vault name validation passed", "vault_name", name)
|
||||||
@@ -292,7 +286,7 @@ func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("vault %s %w", name, ErrVaultNotFound)
|
return fmt.Errorf("%w: %s", ErrVaultNotFound, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the currentvault file with just the vault name
|
// Create or update the currentvault file with just the vault name
|
||||||
|
|||||||
@@ -127,10 +127,7 @@ func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool)
|
|||||||
if !isValidSecretName(name) {
|
if !isValidSecretName(name) {
|
||||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||||
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("%w: '%s'", ErrInvalidSecretName, name)
|
||||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
|
||||||
ErrInvalidSecretName, name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.Debug("Secret name validation passed", "secret_name", name)
|
secret.Debug("Secret name validation passed", "secret_name", name)
|
||||||
@@ -359,7 +356,7 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
|
|||||||
// GetSecretObject retrieves a Secret object with metadata loaded from this vault
|
// GetSecretObject retrieves a Secret object with metadata loaded from this vault
|
||||||
func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
||||||
if !isValidSecretName(name) {
|
if !isValidSecretName(name) {
|
||||||
return nil, fmt.Errorf("%w: %s", ErrInvalidSecretName, name)
|
return nil, fmt.Errorf("%w: '%s'", ErrInvalidSecretName, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check if the secret exists by checking for the metadata file
|
// First check if the secret exists by checking for the metadata file
|
||||||
@@ -379,7 +376,7 @@ func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("secret %s %w", name, ErrSecretNotFound)
|
return nil, fmt.Errorf("%w: %s", ErrSecretNotFound, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Secret object
|
// Create a Secret object
|
||||||
@@ -496,7 +493,7 @@ func (v *Vault) CopySecretAllVersions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return fmt.Errorf("source secret '%s' %w", srcSecretName, ErrNoVersions)
|
return fmt.Errorf("%w: %s", ErrNoVersions, srcSecretName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current version name
|
// Get current version name
|
||||||
@@ -567,10 +564,7 @@ func (v *Vault) prepareSecretDir(
|
|||||||
secret.Debug("Secret already exists and force not specified",
|
secret.Debug("Secret already exists and force not specified",
|
||||||
"secret_name", name, "secret_dir", secretDir)
|
"secret_name", name, "secret_dir", secretDir)
|
||||||
|
|
||||||
return true, nil, fmt.Errorf(
|
return true, nil, fmt.Errorf("%w: %s", ErrSecretExists, name)
|
||||||
"secret %s %w (use --force to overwrite)",
|
|
||||||
name, ErrSecretExists,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current version to update its notAfter timestamp
|
// Get the current version to update its notAfter timestamp
|
||||||
@@ -631,10 +625,7 @@ func (v *Vault) resolveSecretVersion(name, version string) (string, error) {
|
|||||||
if !isValidSecretName(name) {
|
if !isValidSecretName(name) {
|
||||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||||
|
|
||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf("%w: '%s'", ErrInvalidSecretName, name)
|
||||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
|
||||||
ErrInvalidSecretName, name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get vault directory
|
// Get vault directory
|
||||||
@@ -660,7 +651,7 @@ func (v *Vault) resolveSecretVersion(name, version string) (string, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
secret.Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
|
secret.Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
|
||||||
|
|
||||||
return "", fmt.Errorf("secret %s %w", name, ErrSecretNotFound)
|
return "", fmt.Errorf("%w: %s", ErrSecretNotFound, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which version to get
|
// Determine which version to get
|
||||||
@@ -691,10 +682,7 @@ func (v *Vault) resolveSecretVersion(name, version string) (string, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
secret.Debug("Version not found", "version", version, "secret_name", name)
|
secret.Debug("Version not found", "version", version, "secret_name", name)
|
||||||
|
|
||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf("%w: %s (secret %s)", ErrVersionNotFound, version, name)
|
||||||
"version %s %w %s",
|
|
||||||
version, ErrVersionNotFound, name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return version, nil
|
return version, nil
|
||||||
@@ -797,10 +785,7 @@ func (v *Vault) prepareCopyDestination(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if exists && !force {
|
if exists && !force {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("%w: %s (vault %s)", ErrSecretExists, destSecretName, v.Name)
|
||||||
"secret '%s' %w in vault '%s' (use --force to overwrite)",
|
|
||||||
destSecretName, ErrSecretExists, v.Name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists && force {
|
if exists && force {
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unlocker == nil {
|
if unlocker == nil {
|
||||||
return fmt.Errorf("unlocker with ID %s %w", unlockerID, ErrUnlockerNotFound)
|
return fmt.Errorf("%w: %s", ErrUnlockerNotFound, unlockerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the unlocker's Remove method
|
// Use the unlocker's Remove method
|
||||||
@@ -307,7 +307,7 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if targetUnlockerDir == "" {
|
if targetUnlockerDir == "" {
|
||||||
return fmt.Errorf("unlocker with ID %s %w", unlockerID, ErrUnlockerNotFound)
|
return fmt.Errorf("%w: %s", ErrUnlockerNotFound, unlockerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/update current-unlocker file with just the unlocker name
|
// Create/update current-unlocker file with just the unlocker name
|
||||||
|
|||||||
@@ -60,15 +60,8 @@ var (
|
|||||||
// is out of range.
|
// is out of range.
|
||||||
ErrInvalidBase85PwdLen = errors.New("pwdLen must be between 10 and 80")
|
ErrInvalidBase85PwdLen = errors.New("pwdLen must be between 10 and 80")
|
||||||
// ErrPasswordTooShort is returned when the derived material is
|
// ErrPasswordTooShort is returned when the derived material is
|
||||||
// shorter than the requested password length. It carries only the
|
// shorter than the requested password length.
|
||||||
// middle of the message, which the caller composes as
|
ErrPasswordTooShort = errors.New("derived password too short")
|
||||||
// "derived password length <n> is shorter than requested length <m>",
|
|
||||||
// so the emitted text is unchanged.
|
|
||||||
ErrPasswordTooShort = errors.New("is shorter than requested length")
|
|
||||||
// ErrEncodedTooShort is returned when the encoded material is shorter
|
|
||||||
// than the requested password length. Composed as
|
|
||||||
// "encoded length <n> is less than requested length <m>".
|
|
||||||
ErrEncodedTooShort = errors.New("is less than requested length")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version bytes for extended keys
|
// Version bytes for extended keys
|
||||||
@@ -384,8 +377,8 @@ func DeriveBase64Password(
|
|||||||
// Slice to the desired password length
|
// Slice to the desired password length
|
||||||
if len(encodedStr) < int(pwdLen) {
|
if len(encodedStr) < int(pwdLen) {
|
||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf(
|
||||||
"derived password length %d %w %d",
|
"%w: derived length %d is shorter than requested length %d",
|
||||||
len(encodedStr), ErrPasswordTooShort, pwdLen,
|
ErrPasswordTooShort, len(encodedStr), pwdLen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,8 +407,8 @@ func DeriveBase85Password(
|
|||||||
// Slice to the desired password length
|
// Slice to the desired password length
|
||||||
if len(encoded) < int(pwdLen) {
|
if len(encoded) < int(pwdLen) {
|
||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf(
|
||||||
"encoded length %d %w %d",
|
"%w: encoded length %d is less than requested length %d",
|
||||||
len(encoded), ErrEncodedTooShort, pwdLen,
|
ErrPasswordTooShort, len(encoded), pwdLen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user