1 Commits
Author SHA1 Message Date
sneak 369bd772cd Build with the local docker daemon; add script/build (closes #44)
check / check (push) Waiting to run
The Makefile exported DOCKER_HOST pointing at one private machine, so
every docker call made through make, `make lint` and `make check`
included, failed everywhere else. The line is gone: docker uses the
local daemon, or a DOCKER_HOST set in the environment.

`make build` now calls the new `script/build`, which stamps the version
and commit as the Makefile did. A VERSION set in the environment now
wins over `git describe`, not only one given as `make build VERSION=x`.
build, clean, install and docker-run are phony; install depends on
build. The vet target is removed: `script/test` runs `go vet` first.

Model: opus-5-5
2026-10-04 05:17:46 +00:00
13 changed files with 91 additions and 488 deletions
+6 -16
View File
@@ -1,13 +1,7 @@
export CGO_ENABLED=1
export DOCKER_HOST := ssh://root@ber1app1.local
# Version information
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
LDFLAGS := -X 'git.eeqj.de/sneak/secret/internal/cli.Version=$(VERSION)' \
-X 'git.eeqj.de/sneak/secret/internal/cli.GitCommit=$(GIT_COMMIT)'
.PHONY: default bootstrap setup test lint fmt fmt-check check docker hooks vet
.PHONY: default bootstrap setup build test lint fmt fmt-check check docker \
docker-run clean install hooks
default: check
@@ -17,13 +11,9 @@ bootstrap:
setup:
@script/setup
build: ./secret
./secret: ./internal/*/*.go ./pkg/*/*.go ./cmd/*/*.go ./go.*
go build -v -ldflags "$(LDFLAGS)" -o $@ cmd/secret/main.go
vet:
go vet ./...
# Build ./secret; `make build VERSION=x` stamps x instead of `git describe`
build:
@script/build
test:
@script/test
@@ -49,7 +39,7 @@ docker-run:
clean:
rm -f ./secret
install: ./secret
install: build
cp ./secret $(HOME)/bin/secret
fmt-check:
+3 -3
View File
@@ -139,9 +139,6 @@ matching.
Moves or renames a secret within the current vault.
- Fails if the destination already exists
- Fails if the destination is the source under another name, such as `foo`
for `Foo` on a case-insensitive filesystem (the macOS default); there, to
change only the case of a name, move the secret to a third name first
- Preserves all versions and metadata
### Version Management
@@ -506,6 +503,9 @@ them. We provide:
`script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (`secret`); used by
other scripts such as `script/docker`
- `script/build` — build the `secret` binary into the repo root, stamping
the version (`VERSION` from the environment, else `git describe`) and
the git commit
- `script/test` — run `go vet` and the test suite (verbose rerun on
failure)
- `script/lint` — run `golangci-lint` in docker only: builds
+10 -26
View File
@@ -25,29 +25,13 @@ Bring the repo into policy compliance in one commit:
# Completed Steps
- 2026-10-04: `secret init` refuses when the default vault exists, and
`secret vault create NAME` when `NAME` does, with "vault NAME already
exists", before writing anything. The check is in `vault.CreateVault`,
which both commands call while holding the state directory lock, so two
creates of one vault at once cannot both pass the check. Before, either
command replaced the vault's metadata, passphrase unlocker and
`longterm.age`, so none of its secrets could be decrypted any more. Both
commands now ask for the unlocker passphrase before creating the vault,
so one stopped at that prompt leaves no vault behind.
- 2026-10-04: The `internal/cli` tests are back to about their time
before the state directory lock
(https://git.eeqj.de/sneak/secret/issues/80). The test that each
changing command waits for the lock releases it as soon as it sees the
command waiting there, instead of after a fixed 100 ms. The two vaults
with passphrase unlockers that the path and move tests start from are
made once and copied for each test.
- 2026-10-04: `secret mv` rejects a move whose destination is the source
under another name, such as `foo` for `Foo` on a case-insensitive
filesystem (the macOS default) or a name reached through a symbolic
link, before changing anything, with or without `--force`, within a
vault and between vaults; before, `--force` removed the destination and
so deleted the secret. A rename that changes only letter case works on a
case-sensitive filesystem as before.
- 2026-10-04: The `Makefile` no longer sets `DOCKER_HOST`, so its docker
targets use the local docker daemon, or whatever `DOCKER_HOST` the
environment sets. `make build` calls the new `script/build`, which
stamps the version (`VERSION` from the environment, else
`git describe`) and the git commit as before. `build`, `clean`,
`install` and `docker-run` are in `.PHONY`; `make install` depends on
`build`. The `vet` target is gone: `script/test` runs `go vet` first.
- 2026-10-04: Lint runs only in docker: `script/lint` builds
`Dockerfile.lint`, where golangci-lint is a build step rebuilt on
every run (`--no-cache-filter`), so an unchanged tree is linted too;
@@ -87,9 +71,9 @@ Bring the repo into policy compliance in one commit:
has one, and to a PGP, keychain or Secure Enclave unlocker added
on the same host and day as another of its type
(https://git.eeqj.de/sneak/secret/issues/71);
- from `init` or `vault create` killed after the passphrase prompt
but before the unlocker is written, a vault with no unlocker,
which `vault create` has already made the current vault;
- from `vault create` stopped at the passphrase prompt, a new vault
with no unlocker that is already the current vault; from `init`
stopped there, the default vault with no unlocker;
- from an unlocker add stopped before its metadata is written, a
directory that `unlocker list` warns about and `unlocker rm`
cannot remove;
-148
View File
@@ -1,148 +0,0 @@
package cli_test
import (
"testing"
"git.eeqj.de/sneak/secret/internal/cli"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)
// TestCreateExistingVaultChangesNothing is a regression test for
// https://git.eeqj.de/sneak/secret/issues/74, where running `secret init`
// a second time, or `secret vault create` with the name of an existing
// vault, replaced that vault's keys, so that none of its secrets could be
// decrypted any more. Each must refuse, change nothing, and leave every
// vault's secret readable through its passphrase unlocker.
//
//nolint:paralleltest // t.Setenv forbids parallel subtests
func TestCreateExistingVaultChangesNothing(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
// `secret init`, `secret vault create work`, `secret vault select
// default`, and the secret "x" in each vault. "work" is then not the
// current vault, which creating it again must not change.
fs := afero.NewMemMapFs()
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
cmd := &cobra.Command{}
require.NoError(t, c.Init(cmd))
require.NoError(t, c.CreateVault(cmd, "work"))
require.NoError(t, c.SelectVault(cmd, "default"))
vaults, err := vault.ListVaults(fs, testStateDir)
require.NoError(t, err)
require.Len(t, vaults, 2)
for _, name := range vaults {
value := memguard.NewBufferFromBytes([]byte("value"))
err := vault.NewVault(fs, testStateDir, name).AddSecret("x", value, false)
require.NoError(t, err)
}
before := snapshotStateDir(t, fs)
tests := []struct {
command string
want string
run func(c *cli.Instance) error
}{
{
"init",
"failed to create default vault: vault default already exists",
func(c *cli.Instance) error { return c.Init(cmd) },
},
{
"vault create default",
"vault default already exists",
func(c *cli.Instance) error { return c.CreateVault(cmd, "default") },
},
{
"vault create work",
"vault work already exists",
func(c *cli.Instance) error { return c.CreateVault(cmd, "work") },
},
}
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
fs := newFsFromSnapshot(t, before)
err := tt.run(cli.NewCLIInstanceWithStateDir(fs, testStateDir))
require.EqualError(t, err, tt.want)
require.Equal(t, before, snapshotStateDir(t, fs))
})
}
// Every case left the state directory exactly as recorded in before, so
// reading each vault's secret once from it shows that it still decrypts
// after each case. Without the mnemonic, reading a secret goes through
// the vault's passphrase unlocker, which is slow.
t.Setenv(secret.EnvMnemonic, "")
for _, name := range vaults {
value, err := vault.NewVault(fs, testStateDir, name).GetSecret("x")
require.NoError(t, err)
require.Equal(t, "value", string(value))
}
}
// TestStopAtPassphrasePromptLeavesNothing is a regression test for the
// review of https://git.eeqj.de/sneak/secret/pulls/82: `secret init` or
// `secret vault create` stopped at the passphrase prompt left a vault with
// no unlocker, which neither command would then create again. Each must ask
// for the passphrase before writing anything.
//
//nolint:paralleltest // t.Setenv forbids parallel subtests
func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)
// Without the passphrase in the environment, both commands prompt for
// it, which fails because the tests do not run in a terminal.
t.Setenv(secret.EnvUnlockPassphrase, "")
// An empty state directory for `secret init`, and one holding the vault
// "default" for `secret vault create work`.
empty := afero.NewMemMapFs()
require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms))
withDefault := afero.NewMemMapFs()
_, err := vault.CreateVault(withDefault, testStateDir, "default")
require.NoError(t, err)
cmd := &cobra.Command{}
tests := []struct {
command string
fs afero.Fs
run func(c *cli.Instance) error
}{
{
"init",
empty,
func(c *cli.Instance) error { return c.Init(cmd) },
},
{
"vault create work",
withDefault,
func(c *cli.Instance) error { return c.CreateVault(cmd, "work") },
},
}
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
before := snapshotStateDir(t, tt.fs)
err := tt.run(cli.NewCLIInstanceWithStateDir(tt.fs, testStateDir))
require.ErrorContains(t, err, "failed to read passphrase")
require.Equal(t, before, snapshotStateDir(t, tt.fs))
})
}
}
+7 -8
View File
@@ -160,14 +160,6 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
errInvalidMnemonicPhrase)
}
// Ask for the unlocker passphrase before creating the vault, so that
// stopping at the prompt leaves no vault without an unlocker behind
passphraseBuffer, err := resolvePassphrase()
if err != nil {
return err
}
defer passphraseBuffer.Destroy()
// Set mnemonic in environment for CreateVault to use
restoreMnemonicEnv := setMnemonicEnv(mnemonicStr)
defer restoreMnemonicEnv()
@@ -183,6 +175,13 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
// Unlock the vault with the derived long-term key
vlt.Unlock(ltIdentity)
// Prompt for passphrase for unlocker
passphraseBuffer, err := resolvePassphrase()
if err != nil {
return err
}
defer passphraseBuffer.Destroy()
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
+18 -40
View File
@@ -2,11 +2,9 @@
package cli
import (
"bytes"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
@@ -27,6 +25,11 @@ const (
// once the lock is free.
lockWait = 10 * time.Second
// heldWait is how long a test watches a command that must wait for the
// lock. A command that takes no lock changes the state directory well
// within it.
heldWait = 100 * time.Millisecond
// testPassphrase protects the passphrase unlockers the tests create.
testPassphrase = "test-passphrase"
@@ -271,12 +274,11 @@ func stateDirModTimes(t *testing.T, fs afero.Fs) map[string]int64 {
}
// setupEveryCommand makes what each command in
// TestChangingCommandsWaitForLock needs: the current vault "work" with two
// versions of "test/secret", the vault "other" without a long-term key, for
// vault import, and the file testInput. There is no vault "default", which
// init creates. If withUnlocker is set, it also gives "work" a passphrase
// unlocker, which is slow. It returns the older version and the unlocker's
// ID.
// TestChangingCommandsWaitForLock needs: the current vault "default" with
// two versions of "test/secret", the vault "other" without a long-term key,
// for vault import, and the file testInput. If withUnlocker is set, it also
// gives "default" a passphrase unlocker, which is slow. It returns the older
// version and the unlocker's ID.
func setupEveryCommand(
t *testing.T, fs afero.Fs, withUnlocker bool,
) (string, string) {
@@ -289,7 +291,7 @@ func setupEveryCommand(
require.NoError(t, err)
require.NoError(t, fs.Remove(filepath.Join(otherDir, "pub.age")))
vlt, err := vault.CreateVault(fs, testStateDir, "work")
vlt, err := vault.CreateVault(fs, testStateDir, "default")
require.NoError(t, err)
addTestSecret(t, vlt, []byte("older"), false)
@@ -321,28 +323,10 @@ func setupEveryCommand(
return versions[1], unlockerID
}
// waitingForLock reports whether a goroutine is stopped in
// vault.LockStateDir, waiting for the in-memory filesystem's lock. The
// stack trace of such a goroutine starts with the reason it waits,
// "[sync.Mutex.Lock]", and names LockStateDir.
func waitingForLock() bool {
stacks := make([]byte, 1<<20)
stacks = stacks[:runtime.Stack(stacks, true)]
for goroutine := range bytes.SplitSeq(stacks, []byte("\n\n")) {
if bytes.Contains(goroutine, []byte("[sync.Mutex.Lock")) &&
bytes.Contains(goroutine, []byte("vault.LockStateDir(")) {
return true
}
}
return false
}
// requireWaitsForLock runs a command, given what setupEveryCommand made,
// while holding the state directory lock. The command must neither finish
// nor change anything before it waits for the lock, and must succeed once
// the lock is released.
// nor change anything while the lock is held, and must succeed once it is
// released.
func requireWaitsForLock(
t *testing.T,
withUnlocker bool,
@@ -371,20 +355,14 @@ func requireWaitsForLock(
go func() { done <- run(cli, olderVersion, unlockerID) }()
timeout := time.After(lockWait)
for !waitingForLock() {
select {
case err := <-done:
t.Fatalf("finished while the lock was held, with error %v", err)
case <-timeout:
t.Fatal("never waited for the lock")
case <-time.After(time.Millisecond):
}
select {
case err := <-done:
t.Fatalf("finished while the lock was held, with error %v", err)
case <-time.After(heldWait):
}
assert.Equal(t, before, stateDirModTimes(t, fs),
"changed the state directory before waiting for the lock")
"changed the state directory while the lock was held")
release()
-130
View File
@@ -1,15 +1,9 @@
package cli_test
import (
"os"
"path/filepath"
"testing"
"git.eeqj.de/sneak/secret/internal/cli"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)
@@ -103,127 +97,3 @@ func TestMoveWithinOtherVaultKeepsCurrentVault(t *testing.T) {
require.Contains(t, after, workSecrets+"y/")
require.NotContains(t, after, workSecrets+"x/")
}
// TestMoveOntoSameSecretUnderAnotherNameIsRejected is a regression test for
// https://git.eeqj.de/sneak/secret/issues/78: on a case-insensitive
// filesystem "Foo" and "foo" are one secret, and `secret mv --force Foo foo`
// removed the destination, which was the source. Symbolic links on the real
// filesystem give one secret two names here: in "default", "y" is a link to
// the secret "x", and the secrets.d of "other" is a link to that of
// "default", so other:x is default:x. Each move must be rejected and leave
// the secret and the links as they were.
//
//nolint:paralleltest // t.Setenv
func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)
const isSame = "is the same secret on this filesystem"
tests := []struct {
command string
source, dest string
force bool
wantErr string
}{
{
"mv --force y x", "y", "x", true,
"secret 'y' cannot be moved onto itself: 'x' " + isSame,
},
{
"mv --force x y", "x", "y", true,
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
},
{
"mv x y", "x", "y", false,
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
},
{
"mv --force default:x other:x", "default:x", "other:x", true,
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
isSame,
},
{
"mv default:x other", "default:x", "other", false,
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
isSame,
},
}
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
fs := afero.NewOsFs()
stateDir := t.TempDir()
vaultsDir := filepath.Join(stateDir, "vaults.d")
// "default" is created last, so it is the current vault.
_, err := vault.CreateVault(fs, stateDir, "other")
require.NoError(t, err)
vlt, err := vault.CreateVault(fs, stateDir, "default")
require.NoError(t, err)
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
require.NoError(t, err)
defaultSecrets := filepath.Join(vaultsDir, "default", "secrets.d")
otherSecrets := filepath.Join(vaultsDir, "other", "secrets.d")
link := filepath.Join(defaultSecrets, "y")
require.NoError(t, os.Symlink("x", link))
require.NoError(t, os.Remove(otherSecrets))
require.NoError(t, os.Symlink(defaultSecrets, otherSecrets))
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
moveErr := c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, tt.force)
value, err := vlt.GetSecret("x")
require.NoError(t, err)
require.Equal(t, "value", string(value))
target, err := os.Readlink(link)
require.NoError(t, err)
require.Equal(t, "x", target)
target, err = os.Readlink(otherSecrets)
require.NoError(t, err)
require.Equal(t, defaultSecrets, target)
require.EqualError(t, moveErr, tt.wantErr)
})
}
}
// TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem checks that where "Foo"
// and "foo" are two secrets, `secret mv --force Foo foo` still replaces "foo"
// with "Foo".
func TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)
fs := afero.NewOsFs()
stateDir := t.TempDir()
vlt, err := vault.CreateVault(fs, stateDir, "default")
require.NoError(t, err)
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
require.NoError(t, err)
_, err = os.Stat(filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "foo"))
if err == nil {
t.Skip("the temporary directory is on a case-insensitive filesystem")
}
err = vlt.AddSecret("foo", memguard.NewBufferFromBytes([]byte("lower")), false)
require.NoError(t, err)
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
err = c.MoveSecret(&cobra.Command{}, "Foo", "foo", true)
require.NoError(t, err)
value, err := vlt.GetSecret("foo")
require.NoError(t, err)
require.Equal(t, "upper", string(value))
_, err = vlt.GetSecret("Foo")
require.ErrorIs(t, err, vault.ErrSecretNotFound)
}
+11 -29
View File
@@ -6,7 +6,6 @@ import (
"os"
"slices"
"strings"
"sync"
"testing"
"git.eeqj.de/sneak/secret/internal/cli"
@@ -33,20 +32,9 @@ const (
missingFile = "/no/such/file"
)
// The state directory newTwoVaultFs copies, recorded by snapshotStateDir.
// Creating a passphrase unlocker is slow by design, so the vaults are made
// once, by the first test that needs them.
//
//nolint:gochecknoglobals // shared by the tests that use newTwoVaultFs
var (
twoVaultsOnce sync.Once
twoVaults map[string]string
)
// newTwoVaultFs returns an in-memory filesystem holding the vaults "work"
// and "default", the current one. Each holds the secret "x" and a
// passphrase unlocker, so both secrets.d and unlockers.d have contents.
// Every call returns a new copy of the same vaults.
//
//nolint:ireturn // afero.Fs is the filesystem abstraction used throughout
func newTwoVaultFs(t *testing.T) afero.Fs {
@@ -54,27 +42,21 @@ func newTwoVaultFs(t *testing.T) afero.Fs {
t.Setenv(secret.EnvMnemonic, testMnemonic)
twoVaultsOnce.Do(func() {
fs := afero.NewMemMapFs()
fs := afero.NewMemMapFs()
for _, name := range []string{"work", "default"} {
vlt, err := vault.CreateVault(fs, testStateDir, name)
require.NoError(t, err)
for _, name := range []string{"work", "default"} {
vlt, err := vault.CreateVault(fs, testStateDir, name)
require.NoError(t, err)
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
require.NoError(t, err)
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
require.NoError(t, err)
_, err = vlt.CreatePassphraseUnlocker(
memguard.NewBufferFromBytes([]byte(testPassphrase)))
require.NoError(t, err)
}
_, err = vlt.CreatePassphraseUnlocker(
memguard.NewBufferFromBytes([]byte(testPassphrase)))
require.NoError(t, err)
}
twoVaults = snapshotStateDir(t, fs)
})
require.NotNil(t, twoVaults, "making the vaults failed in an earlier test")
return newFsFromSnapshot(t, twoVaults)
return fs
}
// snapshotStateDir maps every file under the state directory to its
-59
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"slices"
"strings"
@@ -891,18 +890,6 @@ func (cli *Instance) moveSecretWithinVault(
destEncoded := strings.ReplaceAll(dest, "/", "%")
destDir := filepath.Join(vaultDir, "secrets.d", destEncoded)
// Removing a destination that is the source under another name, such as
// "foo" for "Foo" on a case-insensitive filesystem, would delete it too.
same, err := cli.sameDirectory(sourceDir, destDir)
if err != nil {
return err
}
if same {
return fmt.Errorf("secret '%s' %w: '%s' is the same secret on "+
"this filesystem", source, errMoveOntoItself, dest)
}
exists, err = afero.DirExists(cli.fs, destDir)
if err != nil {
return fmt.Errorf("failed to check if destination secret exists: %w", err)
@@ -929,31 +916,6 @@ func (cli *Instance) moveSecretWithinVault(
return nil
}
// sameDirectory reports whether the existing directory dir and the path
// other are one directory under two names, as secrets.d/Foo and
// secrets.d/foo are on a case-insensitive filesystem, or a directory and a
// symbolic link to it. Removing other to make room for dir would then delete
// dir. It is false if other does not exist, and always false on the
// in-memory filesystem, which has no such aliasing and whose files
// os.SameFile does not compare.
func (cli *Instance) sameDirectory(dir, other string) (bool, error) {
dirInfo, err := cli.fs.Stat(dir)
if err != nil {
return false, fmt.Errorf("failed to check %s: %w", dir, err)
}
otherInfo, err := cli.fs.Stat(other)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("failed to check %s: %w", other, err)
}
return os.SameFile(dirInfo, otherInfo), nil
}
// moveSecretCrossVault handles moving between two different vaults. Its
// caller, MoveSecret, has already checked both secret names and that both
// vaults exist.
@@ -978,27 +940,6 @@ func (cli *Instance) moveSecretCrossVault(
srcSecretName, errSecretNotFound, srcVault.Name)
}
// The source is removed after the copy, so a destination that is the
// source under another name would be lost with it.
destVaultDir, err := destVault.GetDirectory()
if err != nil {
return fmt.Errorf("failed to get destination vault directory: %w", err)
}
destStorageName := strings.ReplaceAll(destSecretName, "/", "%")
destSecretDir := filepath.Join(destVaultDir, "secrets.d", destStorageName)
same, err := cli.sameDirectory(srcSecretDir, destSecretDir)
if err != nil {
return err
}
if same {
return fmt.Errorf("secret '%s:%s' %w: '%s:%s' is the same secret on "+
"this filesystem", srcVault.Name, srcSecretName, errMoveOntoItself,
destVault.Name, destSecretName)
}
// Unlock destination vault (will fail if neither mnemonic nor unlocker available)
_, err = destVault.GetOrDeriveLongTermKey()
if err != nil {
+7 -8
View File
@@ -309,14 +309,6 @@ func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
return errInvalidMnemonicPhrase
}
// Ask for the unlocker passphrase before creating the vault, so that
// stopping at the prompt leaves no vault without an unlocker behind
passphraseBuffer, err := resolvePassphrase()
if err != nil {
return err
}
defer passphraseBuffer.Destroy()
// Set mnemonic in environment for CreateVault to use
restoreMnemonicEnv := setMnemonicEnv(mnemonicStr)
defer restoreMnemonicEnv()
@@ -344,6 +336,13 @@ func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
// Unlock the vault with the derived long-term key
vlt.Unlock(ltIdentity)
// Get or prompt for passphrase
passphraseBuffer, err := resolvePassphrase()
if err != nil {
return err
}
defer passphraseBuffer.Destroy()
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
-4
View File
@@ -26,10 +26,6 @@ var (
// as "vault <name> does not exist".
ErrVaultNotFound = errors.New("does not exist")
// ErrVaultExists indicates that a vault to be created already exists.
// Composed as "vault <name> already exists".
ErrVaultExists = errors.New("already exists")
// ErrNilValueBuffer indicates a nil value buffer was supplied.
ErrNilValueBuffer = errors.New("value buffer is nil")
+3 -17
View File
@@ -191,11 +191,7 @@ func processMnemonicForVault(
return derivationIndex, publicKeyHash, familyHash, nil
}
// CreateVault creates a new vault and selects it as the current vault. It
// refuses a vault that already exists before writing anything: creating it
// again would replace its keys, and its secrets could no longer be
// decrypted. The commands that call it hold the state directory lock, so no
// other command can create the vault between the check and the writes.
// CreateVault creates a new vault
func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
@@ -211,22 +207,12 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
secret.Debug("Vault name validation passed", "vault_name", name)
vaultDir := filepath.Join(stateDir, "vaults.d", name)
exists, err := afero.DirExists(fs, vaultDir)
if err != nil {
return nil, fmt.Errorf("failed to check if vault exists: %w", err)
}
if exists {
return nil, fmt.Errorf("vault %s %w", name, ErrVaultExists)
}
// Create vault directory structure
vaultDir := filepath.Join(stateDir, "vaults.d", name)
secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
// Create main vault directory
err = fs.MkdirAll(vaultDir, secret.DirPerms)
err := fs.MkdirAll(vaultDir, secret.DirPerms)
if err != nil {
return nil, fmt.Errorf("failed to create vault directory: %w", err)
}
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# script/build: build the `secret` binary into the repo root, with its
# version and git commit stamped in (`secret info` shows both).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
# CGO is required (Makefile exports this too)
export CGO_ENABLED=1
# A VERSION set in the environment wins (`make build VERSION=x`, as
# the Dockerfile does); otherwise `git describe` of this checkout.
version="${VERSION:-}"
if [ -z "$version" ]; then
version="$(git describe --tags --always --dirty 2>/dev/null ||
echo dev)"
fi
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
pkg=git.eeqj.de/sneak/secret/internal/cli
go build -v \
-ldflags "-X '$pkg.Version=$version' -X '$pkg.GitCommit=$commit'" \
-o secret ./cmd/secret
}
main "$@"