Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682bf751e7 |
@@ -91,9 +91,6 @@ Lists all available vaults. The current vault is marked.
|
|||||||
|
|
||||||
Creates a new vault with the specified name.
|
Creates a new vault with the specified name.
|
||||||
|
|
||||||
**Vault Name Format:** only lowercase ASCII letters, digits, `.`, `-` and `_`
|
|
||||||
are allowed, and a name must not be empty, `.` or `..`.
|
|
||||||
|
|
||||||
#### `secret vault select <name>`
|
#### `secret vault select <name>`
|
||||||
|
|
||||||
Switches to the specified vault for subsequent operations.
|
Switches to the specified vault for subsequent operations.
|
||||||
@@ -326,9 +323,7 @@ line or in a CI job, they end up in shell history and CI logs. `secret` unsets
|
|||||||
each one as soon as it has read it, so that the programs it runs itself, such
|
each one as soon as it has read it, so that the programs it runs itself, such
|
||||||
as `gpg`, do not inherit it, but that erases nothing: the environment the
|
as `gpg`, do not inherit it, but that erases nothing: the environment the
|
||||||
process started with, and its memory, still hold the value. The interactive
|
process started with, and its memory, still hold the value. The interactive
|
||||||
prompt, which every command except `secret vault import` offers when the
|
prompt, used when the variable is not set, is the safer default.
|
||||||
variable is not set, is the safer default; `secret vault import` has no prompt
|
|
||||||
and needs both variables.
|
|
||||||
|
|
||||||
## Security Features
|
## Security Features
|
||||||
|
|
||||||
|
|||||||
@@ -37,15 +37,6 @@ Bring the repo into policy compliance in one commit:
|
|||||||
mnemonic into the environment. Unsetting erases nothing: the starting
|
mnemonic into the environment. Unsetting erases nothing: the starting
|
||||||
environment (`/proc/<pid>/environ`) and memory still hold the value. The
|
environment (`/proc/<pid>/environ`) and memory still hold the value. The
|
||||||
README warns against both variables.
|
README warns against both variables.
|
||||||
- 2026-10-04: A vault name may use only lowercase ASCII letters, digits,
|
|
||||||
`.`, `-` and `_`, and must not be empty, `.` or `..`
|
|
||||||
(https://git.eeqj.de/sneak/secret/issues/68); the error and `README.md`
|
|
||||||
state the rule. `vault create`, `vault import`, `vault select`,
|
|
||||||
`vault remove`, both vault names of `mv` and shell completion of a
|
|
||||||
`vault:secret` argument check the name as typed with
|
|
||||||
`vault.ValidateVaultName` before building any path from it. Before,
|
|
||||||
`vault import ..` wrote a long-term key and an unlocker into the state
|
|
||||||
directory itself, and `vault select ..` made that the current vault.
|
|
||||||
- 2026-10-04: `script/cibuild` runs the checks again on an unchanged
|
- 2026-10-04: `script/cibuild` runs the checks again on an unchanged
|
||||||
tree (https://git.eeqj.de/sneak/secret/issues/54). It passes the
|
tree (https://git.eeqj.de/sneak/secret/issues/54). It passes the
|
||||||
current time as the `CHECK_EPOCH` build argument, which both the lint
|
current time as the `CHECK_EPOCH` build argument, which both the lint
|
||||||
|
|||||||
@@ -123,9 +123,7 @@ func getVaultNamesCompletionFunc(fs afero.Fs, stateDir string) func(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// completeVaultQualifiedSecrets completes "vault:secret" references once a
|
// completeVaultQualifiedSecrets completes "vault:secret" references once a
|
||||||
// colon is present in the input. It completes nothing when the vault part
|
// colon is present in the input
|
||||||
// is not a valid vault name, so that a name such as ".." cannot list a
|
|
||||||
// directory outside vaults.d.
|
|
||||||
func completeVaultQualifiedSecrets(
|
func completeVaultQualifiedSecrets(
|
||||||
fs afero.Fs, stateDir, toComplete string,
|
fs afero.Fs, stateDir, toComplete string,
|
||||||
) []string {
|
) []string {
|
||||||
@@ -136,10 +134,6 @@ func completeVaultQualifiedSecrets(
|
|||||||
vaultName := parts[0]
|
vaultName := parts[0]
|
||||||
secretPrefix := parts[1]
|
secretPrefix := parts[1]
|
||||||
|
|
||||||
if vault.ValidateVaultName(vaultName) != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
vlt := vault.NewVault(fs, stateDir, vaultName)
|
vlt := vault.NewVault(fs, stateDir, vaultName)
|
||||||
|
|
||||||
secrets, err := vlt.ListSecrets()
|
secrets, err := vlt.ListSecrets()
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
//nolint:testpackage // white-box test of unexported internals
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestVaultSecretCompletionRejectsInvalidVaultName is a regression test for
|
|
||||||
// https://git.eeqj.de/sneak/secret/issues/68: completing a `vault:secret`
|
|
||||||
// argument lists nothing when the vault part is not a valid vault name, even
|
|
||||||
// where that name, joined onto vaults.d, leads to a secrets.d directory.
|
|
||||||
func TestVaultSecretCompletionRejectsInvalidVaultName(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
stateDir = "/state"
|
|
||||||
dirPerm = 0o700
|
|
||||||
)
|
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
|
||||||
|
|
||||||
// The vault "work" holds the secret "x". So does every directory an
|
|
||||||
// invalid name below would lead to from vaults.d.
|
|
||||||
for _, vaultName := range []string{"work", ".", "..", "a/b"} {
|
|
||||||
secretDir := filepath.Join(stateDir, "vaults.d", vaultName, "secrets.d", "x")
|
|
||||||
require.NoError(t, fs.MkdirAll(secretDir, dirPerm))
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"work:x"},
|
|
||||||
completeVaultQualifiedSecrets(fs, stateDir, "work:"))
|
|
||||||
|
|
||||||
for _, toComplete := range []string{".:", "..:", "a/b:"} {
|
|
||||||
assert.Empty(t, completeVaultQualifiedSecrets(fs, stateDir, toComplete),
|
|
||||||
"completing %q", toComplete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,25 +47,26 @@ func TestRejectedMoveWithinVaultLeavesStateUnchanged(t *testing.T) {
|
|||||||
"mv work:nosuch work:y", "work:nosuch", "work:y", false,
|
"mv work:nosuch work:y", "work:nosuch", "work:y", false,
|
||||||
"secret 'nosuch' not found",
|
"secret 'nosuch' not found",
|
||||||
},
|
},
|
||||||
// Only an existing vault is used.
|
// Only an existing vault is used, so ".." cannot reach the state
|
||||||
|
// directory itself.
|
||||||
{
|
{
|
||||||
"mv --force nosuch:x nosuch:y", "nosuch:x", "nosuch:y", true,
|
"mv --force ..:x ..:y", "..:x", "..:y", true,
|
||||||
"vault 'nosuch' does not exist",
|
"vault '..' does not exist",
|
||||||
},
|
},
|
||||||
// Each of these spells "work" a second way. The spelling is not a
|
// Each of these spells "work" a second way. The spelling is not an
|
||||||
// valid vault name, so the move is not taken for a move between two
|
// existing vault name, so the move is not taken for a move between
|
||||||
// vaults, which would delete the destination, here the source.
|
// two vaults, which would delete the destination, here the source.
|
||||||
{
|
{
|
||||||
"mv --force work:x work/:x", workX, "work/:x", true,
|
"mv --force work:x work/:x", workX, "work/:x", true,
|
||||||
vault.ValidateVaultName("work/").Error(),
|
"vault 'work/' does not exist",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mv --force work/:x work:", "work/:x", "work:", true,
|
"mv --force work/:x work:", "work/:x", "work:", true,
|
||||||
vault.ValidateVaultName("work/").Error(),
|
"vault 'work/' does not exist",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mv --force work:x ./work:x", workX, "./work:x", true,
|
"mv --force work:x ./work:x", workX, "./work:x", true,
|
||||||
vault.ValidateVaultName("./work").Error(),
|
"vault './work' does not exist",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,65 +302,6 @@ func TestInvalidVersionLeavesVaultsUnchanged(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestInvalidVaultNameLeavesStateUnchanged is a regression test for
|
|
||||||
// https://git.eeqj.de/sneak/secret/issues/68, where
|
|
||||||
// `secret vault import ..` wrote a long-term key and an unlocker into the
|
|
||||||
// state directory itself, and `secret vault select ..` made it the current
|
|
||||||
// vault. Each command that takes a vault name must reject an invalid one
|
|
||||||
// before building a path from it. The instance is given the mnemonic and
|
|
||||||
// the passphrase, and moves and removals use --force, so that only the name
|
|
||||||
// check stands in the way.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // the cases share cmd
|
|
||||||
func TestInvalidVaultNameLeavesStateUnchanged(t *testing.T) {
|
|
||||||
before := snapshotStateDir(t, newTwoVaultFs(t))
|
|
||||||
|
|
||||||
mnemonic := testMnemonicBuffer(t)
|
|
||||||
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
||||||
t.Cleanup(passphrase.Destroy)
|
|
||||||
|
|
||||||
cmd := &cobra.Command{}
|
|
||||||
|
|
||||||
// Each command is a format with %q where the vault name goes.
|
|
||||||
commands := []struct {
|
|
||||||
command string
|
|
||||||
run func(c *cli.Instance, name string) error
|
|
||||||
}{
|
|
||||||
{"vault create %q", func(c *cli.Instance, name string) error {
|
|
||||||
return c.CreateVault(cmd, name)
|
|
||||||
}},
|
|
||||||
{"vault import %q", func(c *cli.Instance, name string) error {
|
|
||||||
return c.VaultImport(cmd, name)
|
|
||||||
}},
|
|
||||||
{"vault select %q", func(c *cli.Instance, name string) error {
|
|
||||||
return c.SelectVault(cmd, name)
|
|
||||||
}},
|
|
||||||
{"vault remove --force %q", func(c *cli.Instance, name string) error {
|
|
||||||
return c.RemoveVault(cmd, name, true)
|
|
||||||
}},
|
|
||||||
{"mv --force %q:x work:x", func(c *cli.Instance, name string) error {
|
|
||||||
return c.MoveSecret(cmd, name+":x", "work:x", true)
|
|
||||||
}},
|
|
||||||
{"mv --force default:x %q:x", func(c *cli.Instance, name string) error {
|
|
||||||
return c.MoveSecret(cmd, "default:x", name+":x", true)
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range commands {
|
|
||||||
for _, name := range []string{"", ".", "..", "a/b"} {
|
|
||||||
t.Run(fmt.Sprintf(tt.command, name), func(t *testing.T) {
|
|
||||||
requireRejectedAndUnchanged(t, before, vault.ValidateVaultName(name),
|
|
||||||
func(c *cli.Instance) error {
|
|
||||||
c.Mnemonic = mnemonic
|
|
||||||
c.UnlockPassphrase = passphrase
|
|
||||||
|
|
||||||
return tt.run(c, name)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRemoveVersionRemovesOnlyThatVersion checks that `secret version rm`
|
// TestRemoveVersionRemovesOnlyThatVersion checks that `secret version rm`
|
||||||
// with a version that is not the current one removes that version and
|
// with a version that is not the current one removes that version and
|
||||||
// changes nothing else.
|
// changes nothing else.
|
||||||
|
|||||||
+5
-11
@@ -831,9 +831,9 @@ func (cli *Instance) moveSecret(
|
|||||||
cmd, vlt, srcSecretName, destSecretName, force)
|
cmd, vlt, srcSecretName, destSecretName, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both vault names must be valid and name existing vaults exactly, so
|
// Both vaults must be existing vaults by exact name, so that two
|
||||||
// that two spellings of one vault, such as "work" and "work/", are never
|
// spellings of one vault, such as "work" and "work/", are never taken for
|
||||||
// taken for two vaults. A named vault does not become the current vault.
|
// two vaults. A named vault does not become the current vault.
|
||||||
srcVault, err := cli.existingVault(srcVaultName)
|
srcVault, err := cli.existingVault(srcVaultName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -853,15 +853,9 @@ func (cli *Instance) moveSecret(
|
|||||||
cmd, srcVault, srcSecretName, destVault, destSecretName, force)
|
cmd, srcVault, srcSecretName, destVault, destSecretName, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
// existingVault returns the vault with the given name, or an error if the
|
// existingVault returns the vault with the given name, or an error if there
|
||||||
// name is not a valid vault name or there is no such vault. Unlike
|
// is none. Unlike vault.SelectVault, it leaves the current vault as it is.
|
||||||
// vault.SelectVault, it leaves the current vault as it is.
|
|
||||||
func (cli *Instance) existingVault(name string) (*vault.Vault, error) {
|
func (cli *Instance) existingVault(name string) (*vault.Vault, error) {
|
||||||
err := vault.ValidateVaultName(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list vaults: %w", err)
|
return nil, fmt.Errorf("failed to list vaults: %w", err)
|
||||||
|
|||||||
@@ -433,11 +433,6 @@ func updateVaultImportMetadata(
|
|||||||
// VaultImport imports a mnemonic into a specific vault, holding the state
|
// VaultImport imports a mnemonic into a specific vault, holding the state
|
||||||
// directory lock while importMnemonic runs
|
// directory lock while importMnemonic runs
|
||||||
func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
||||||
err := vault.ValidateVaultName(vaultName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -589,11 +584,6 @@ func (cli *Instance) switchAwayFromVault(
|
|||||||
// RemoveVault removes a vault, holding the state directory lock while
|
// RemoveVault removes a vault, holding the state directory lock while
|
||||||
// removeVault runs
|
// removeVault runs
|
||||||
func (cli *Instance) RemoveVault(cmd *cobra.Command, name string, force bool) error {
|
func (cli *Instance) RemoveVault(cmd *cobra.Command, name string, force bool) error {
|
||||||
err := vault.ValidateVaultName(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
package secret
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -23,7 +22,7 @@ const testMnemonicValue = "abandon abandon abandon abandon abandon abandon " +
|
|||||||
"abandon abandon abandon abandon abandon about"
|
"abandon abandon abandon abandon abandon about"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errMnemonicNotSet = errors.New("mock vault has no mnemonic")
|
errMnemonicNotSet = errors.New("SB_SECRET_MNEMONIC not set")
|
||||||
errNotImplementedInMock = errors.New("not implemented in mock")
|
errNotImplementedInMock = errors.New("not implemented in mock")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ type MockVault struct {
|
|||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
directory string
|
directory string
|
||||||
derivationIndex uint32
|
derivationIndex uint32
|
||||||
mnemonic *memguard.LockedBuffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockVault) GetDirectory() (string, error) {
|
func (m *MockVault) GetDirectory() (string, error) {
|
||||||
@@ -63,11 +61,12 @@ func (m *MockVault) AddSecret(name string, value *memguard.LockedBuffer, _ bool)
|
|||||||
ltPubKeyPath := filepath.Join(m.directory, "pub.age")
|
ltPubKeyPath := filepath.Join(m.directory, "pub.age")
|
||||||
|
|
||||||
// Derive long-term key using the vault's derivation index
|
// Derive long-term key using the vault's derivation index
|
||||||
if m.mnemonic == nil {
|
mnemonic := os.Getenv(EnvMnemonic)
|
||||||
|
if mnemonic == "" {
|
||||||
return errMnemonicNotSet
|
return errMnemonicNotSet
|
||||||
}
|
}
|
||||||
|
|
||||||
ltIdentity, err := agehd.DeriveIdentity(m.mnemonic.String(), m.derivationIndex)
|
ltIdentity, err := agehd.DeriveIdentity(mnemonic, m.derivationIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -235,13 +234,13 @@ func verifySecretFiles(t *testing.T, fs afero.Fs, vaultDir, secretName string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:paralleltest // subtests share one vault, order matters
|
//nolint:paralleltest // uses t.Setenv (process-global environment)
|
||||||
func TestPerSecretKeyFunctionality(t *testing.T) {
|
func TestPerSecretKeyFunctionality(t *testing.T) {
|
||||||
// Create an in-memory filesystem for testing
|
// Create an in-memory filesystem for testing
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonicValue))
|
// Set test mnemonic for direct encryption/decryption
|
||||||
defer mnemonic.Destroy()
|
t.Setenv(EnvMnemonic, testMnemonicValue)
|
||||||
|
|
||||||
// Set up a test vault structure
|
// Set up a test vault structure
|
||||||
baseDir := "/test-config/berlin.sneak.pkg.secret"
|
baseDir := "/test-config/berlin.sneak.pkg.secret"
|
||||||
@@ -255,7 +254,6 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
|
|||||||
fs: fs,
|
fs: fs,
|
||||||
directory: vaultDir,
|
directory: vaultDir,
|
||||||
derivationIndex: 0,
|
derivationIndex: 0,
|
||||||
mnemonic: mnemonic,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
@@ -312,45 +310,26 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSecretGetValueWithMnemonicUsesVaultDerivationIndex checks that
|
func TestSecretGetValueWithEnvMnemonicUsesVaultDerivationIndex(t *testing.T) {
|
||||||
// GetValue, given the mnemonic, derives the long-term key at the derivation
|
// This test demonstrates the bug where GetValue uses hardcoded index 0
|
||||||
// index in the vault's metadata. At index 0 it could not decrypt the secret,
|
// instead of the vault's actual derivation index when using environment mnemonic
|
||||||
// which was encrypted to the key at index 1.
|
|
||||||
func TestSecretGetValueWithMnemonicUsesVaultDerivationIndex(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
// Set up test mnemonic
|
||||||
vaultDir := "/test-config/vaults.d/test-vault"
|
t.Setenv(EnvMnemonic, testMnemonicValue)
|
||||||
|
|
||||||
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonicValue))
|
// Create temporary directory for vaults
|
||||||
defer mnemonic.Destroy()
|
fs := afero.NewOsFs()
|
||||||
|
tempDir, err := afero.TempDir(fs, "", "secret-test-")
|
||||||
vlt := &MockVault{
|
|
||||||
name: "test-vault",
|
|
||||||
fs: fs,
|
|
||||||
directory: vaultDir,
|
|
||||||
derivationIndex: 1,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata, err := json.Marshal(VaultMetadata{DerivationIndex: vlt.derivationIndex})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, fs.MkdirAll(vaultDir, DirPerms))
|
|
||||||
|
|
||||||
err = afero.WriteFile(
|
|
||||||
fs, filepath.Join(vaultDir, "vault-metadata.json"), metadata, FilePerms)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
secretName, secretValue := "x", "value"
|
defer func() {
|
||||||
|
_ = fs.RemoveAll(tempDir)
|
||||||
|
}()
|
||||||
|
|
||||||
err = vlt.AddSecret(secretName,
|
stateDir := filepath.Join(tempDir, ".secret")
|
||||||
memguard.NewBufferFromBytes([]byte(secretValue)), false)
|
require.NoError(t, fs.MkdirAll(stateDir, 0o700))
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
value, err := NewSecret(vlt, secretName).GetValue(nil, mnemonic)
|
// This test is now in the integration test file where it can use real vaults
|
||||||
require.NoError(t, err)
|
// The bug is demonstrated there - see test31EnvMnemonicUsesVaultDerivationIndex
|
||||||
|
t.Log("This test demonstrates the bug in the integration test file")
|
||||||
defer value.Destroy()
|
|
||||||
|
|
||||||
require.Equal(t, secretValue, value.String())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ var (
|
|||||||
"derived public key does not match vault: mnemonic may be incorrect",
|
"derived public key does not match vault: mnemonic may be incorrect",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidVaultName indicates a vault name that breaks the naming
|
// ErrInvalidVaultName indicates a vault name that does not match the
|
||||||
// rule: only lowercase ASCII letters, digits, '.', '-' and '_'; not
|
// allowed pattern [a-z0-9.\-_]+. Composed as
|
||||||
// empty, "." or "..". Composed by ValidateVaultName as
|
// "invalid vault name '<name>': must match pattern [a-z0-9.\-_]+".
|
||||||
// "invalid vault name '<name>': <the rule>".
|
|
||||||
ErrInvalidVaultName = errors.New("invalid vault name")
|
ErrInvalidVaultName = errors.New("invalid vault name")
|
||||||
|
|
||||||
// ErrVaultNotFound indicates the named vault does not exist. Composed
|
// ErrVaultNotFound indicates the named vault does not exist. Composed
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidVaultName reports whether name is a valid vault name: only
|
// isValidVaultName validates vault names according to the format [a-z0-9\.\-\_]+
|
||||||
// lowercase ASCII letters, digits, '.', '-' and '_', and not empty, "." or
|
// Note: We don't allow slashes in vault names unlike secret names
|
||||||
// "..". With no path separator allowed, a vault is always one directory
|
|
||||||
// directly under vaults.d.
|
|
||||||
func isValidVaultName(name string) bool {
|
func isValidVaultName(name string) bool {
|
||||||
if name == "" || name == "." || name == ".." {
|
if name == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,21 +36,6 @@ func isValidVaultName(name string) bool {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateVaultName returns an error wrapping ErrInvalidVaultName when name
|
|
||||||
// is not a valid vault name. Call it on the name exactly as the user gave it,
|
|
||||||
// before building any path from it.
|
|
||||||
func ValidateVaultName(name string) error {
|
|
||||||
if !isValidVaultName(name) {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w '%s': only lowercase ASCII letters, digits, '.', '-' and '_' "+
|
|
||||||
"are allowed, and a name must not be empty, '.' or '..'",
|
|
||||||
ErrInvalidVaultName, name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveVaultSymlink reads the currentvault file to get the path to the current vault
|
// ResolveVaultSymlink reads the currentvault file to get the path to the current vault
|
||||||
// The file contains just the vault name (e.g., "default")
|
// The file contains just the vault name (e.g., "default")
|
||||||
func ResolveVaultSymlink(fs afero.Fs, currentVaultPath string) (string, error) {
|
func ResolveVaultSymlink(fs afero.Fs, currentVaultPath string) (string, error) {
|
||||||
@@ -220,11 +203,14 @@ func CreateVault(
|
|||||||
) (*Vault, error) {
|
) (*Vault, error) {
|
||||||
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
|
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
|
||||||
|
|
||||||
err := ValidateVaultName(name)
|
// Validate vault name
|
||||||
if err != nil {
|
if !isValidVaultName(name) {
|
||||||
secret.Debug("Invalid vault name provided", "vault_name", name)
|
secret.Debug("Invalid vault name provided", "vault_name", name)
|
||||||
|
|
||||||
return nil, err
|
return nil, fmt.Errorf(
|
||||||
|
"%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)
|
||||||
@@ -306,11 +292,14 @@ func CreateVault(
|
|||||||
func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
||||||
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
|
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
|
||||||
|
|
||||||
err := ValidateVaultName(name)
|
// Validate vault name
|
||||||
if err != nil {
|
if !isValidVaultName(name) {
|
||||||
secret.Debug("Invalid vault name provided", "vault_name", name)
|
secret.Debug("Invalid vault name provided", "vault_name", name)
|
||||||
|
|
||||||
return err
|
return fmt.Errorf(
|
||||||
|
"%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)
|
||||||
|
|||||||
Reference in New Issue
Block a user