Commands that change the vault hold an exclusive lock on DIR/.lock and readers hold a shared one, taken after the passphrase prompt. A blocked process says it is waiting. Unix uses fcntl locks and Windows uses LockFileEx, both released by the OS if the process dies. Plan 9 and WebAssembly fall back to an exclusive lock file. build.sh cross-compiles static binaries for every Go target that builds without cgo (43 today, armv6 and armv7 for 32-bit ARM Linux) and writes SHA256SUMS.
178 lines
4.9 KiB
Go
178 lines
4.9 KiB
Go
// Copyright 2026 The vault Authors.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestKeyFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "key")
|
|
if err := writeKeyFile(path, "pass", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := unlockKeyFile(path, "wrong"); err == nil {
|
|
t.Fatal("wrong passphrase unlocked the key")
|
|
}
|
|
if _, err := unlockKeyFile(path, "pass"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func newTestStore(t *testing.T, keyPath, pass string) *store {
|
|
t.Helper()
|
|
id, err := unlockKeyFile(keyPath, pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s, err := openStore(filepath.Join(filepath.Dir(keyPath), "store"), id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestStoreLifecycle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
keyPath := filepath.Join(dir, "key")
|
|
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := initStore(filepath.Join(dir, "store")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := newTestStore(t, keyPath, "pass")
|
|
src := filepath.Join(dir, "in")
|
|
want := bytes.Repeat([]byte("x"), 70000)
|
|
os.WriteFile(src, want, 0o640)
|
|
|
|
cat, _ := s.catalogue()
|
|
if _, err := s.add(cat, src, "a/b"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if replaced, err := s.add(cat, src, "a/b"); err != nil || !replaced {
|
|
t.Fatalf("replace: %v %v", replaced, err)
|
|
}
|
|
if _, err := s.add(cat, src, "c"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cat, _ = s.catalogue(); len(cat) != 2 || len(cat["a/b"]) != 1 {
|
|
t.Fatalf("catalogue = %v", cat)
|
|
}
|
|
if err := s.rename("a/b", "d"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := filepath.Join(dir, "out")
|
|
if err := s.get("d", out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := os.ReadFile(out); !bytes.Equal(got, want) {
|
|
t.Fatal("round trip mismatch")
|
|
}
|
|
if err := s.get("d", out); err == nil {
|
|
t.Fatal("get overwrote an existing file")
|
|
}
|
|
|
|
// Swapping one object for another must be detected.
|
|
cat, _ = s.catalogue()
|
|
d, c := cat["d"][0], cat["c"][0]
|
|
objects := filepath.Join(s.root, objectsDir)
|
|
data, _ := os.ReadFile(filepath.Join(objects, c.Object))
|
|
os.WriteFile(filepath.Join(objects, d.Object), data, 0o600)
|
|
if err := s.get("d", filepath.Join(dir, "swapped")); err == nil {
|
|
t.Fatal("swapped object was accepted")
|
|
}
|
|
|
|
if err := s.delete("c"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cat, _ = s.catalogue(); len(cat) != 1 {
|
|
t.Fatalf("after delete: %v", cat)
|
|
}
|
|
}
|
|
|
|
func TestDecoy(t *testing.T) {
|
|
dir := t.TempDir()
|
|
keyPath := filepath.Join(dir, "key")
|
|
if err := writeKeyFile(keyPath, "real", "decoy"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := initStore(filepath.Join(dir, "store")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src := filepath.Join(dir, "in")
|
|
os.WriteFile(src, []byte("data"), 0o600)
|
|
real, decoy := newTestStore(t, keyPath, "real"), newTestStore(t, keyPath, "decoy")
|
|
for s, name := range map[*store]string{real: "secret", decoy: "shopping"} {
|
|
cat, err := s.catalogue()
|
|
if err != nil || len(cat) != 0 {
|
|
t.Fatalf("fresh vault catalogue = %v, %v", cat, err)
|
|
}
|
|
if _, err := s.add(cat, src, name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for s, name := range map[*store]string{real: "secret", decoy: "shopping"} {
|
|
if cat, _ := s.catalogue(); len(cat) != 1 || cat[name] == nil {
|
|
t.Fatalf("catalogue = %v, want only %s", cat, name)
|
|
}
|
|
}
|
|
if _, err := unlockKeyFile(keyPath, "neither"); err == nil {
|
|
t.Fatal("wrong passphrase unlocked the key")
|
|
}
|
|
entries, _ := os.ReadDir(filepath.Join(dir, "store", recordsDir))
|
|
if len(entries) < fillerMin+2 {
|
|
t.Fatalf("only %d records; filler missing", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestLockExcludesOtherProcess runs a copy of the test binary that holds the
|
|
// exclusive lock, then checks this process has to wait for it.
|
|
func TestLockExcludesOtherProcess(t *testing.T) {
|
|
if dir := os.Getenv("VAULT_TEST_LOCK_HOLDER"); dir != "" {
|
|
release, err := lockVault(dir, true)
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
os.Stdout.WriteString("locked\n")
|
|
io.Copy(io.Discard, os.Stdin) // hold until the parent closes stdin
|
|
release()
|
|
os.Exit(0)
|
|
}
|
|
dir := t.TempDir()
|
|
holder := exec.Command(os.Args[0], "-test.run=^TestLockExcludesOtherProcess$")
|
|
holder.Env = append(os.Environ(), "VAULT_TEST_LOCK_HOLDER="+dir)
|
|
stdin, _ := holder.StdinPipe()
|
|
stdout, _ := holder.StdoutPipe()
|
|
if err := holder.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer holder.Wait()
|
|
if line, _ := bufio.NewReader(stdout).ReadString('\n'); line != "locked\n" {
|
|
t.Fatalf("holder said %q", line)
|
|
}
|
|
waited := make(chan bool, 1)
|
|
go func() {
|
|
time.Sleep(200 * time.Millisecond)
|
|
stdin.Close() // let the holder go only after we have started waiting
|
|
}()
|
|
for _, exclusive := range []bool{false, true} {
|
|
release, err := acquireLock(filepath.Join(dir, lockName), exclusive, func() { waited <- true })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
if !exclusive && len(waited) == 0 {
|
|
t.Fatal("shared lock did not wait for the exclusive holder")
|
|
}
|
|
}
|
|
}
|