Add cp, folder mv, get -r, and unlock/lock to remember the key
Filesystem basics: mv now moves folders as well as files, and like mv(1) a NEW ending in / or naming an existing folder means into it. cp copies a file, or a folder with -r, by writing new records that share the original's objects, so it is instant and takes no space; rm already keeps objects another record still uses. New records go first, so an interrupted move leaves both names, never neither. get -r extracts a folder, keeping its structure. vault unlock [--for 5m] checks the passphrase and starts the binary again as a hidden __agent process, handing it the suite ID and seed over a pipe. The agent keeps them in mlocked memory, with core dumps off and (on Linux) PR_SET_DUMPABLE cleared, serves them on a Unix socket in a 0700 per-user directory whose owner is checked on every use, and wipes them and exits at a hard deadline or on vault lock. Commands ask it first unless VAULT_PASSPHRASE is set, and genkey stops any agent left for that path. Guide, man pages (new vault-cp, vault-unlock, vault-lock), README and screenshots updated; the guide's error table is split so it fits.
This commit is contained in:
+116
-1
@@ -9,6 +9,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -75,7 +76,11 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
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 {
|
||||
pairs, err := transfers(cat, "a/b", "d", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.copyAll(cat, pairs, true, func(string, string) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := filepath.Join(dir, "out")
|
||||
@@ -575,3 +580,113 @@ func TestLs(t *testing.T) {
|
||||
t.Fatal("ls of a missing folder succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyMove(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"), 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := newTestStore(t, keyPath, "pass")
|
||||
cat, _ := s.catalogue()
|
||||
src := filepath.Join(dir, "in")
|
||||
for _, name := range []string{"a/x", "a/b/y", "ab/z", "c/w"} {
|
||||
os.WriteFile(src, []byte(name), 0o600)
|
||||
if _, err := s.add(cat, src, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
run := func(src, dst string, recursive, move bool) error {
|
||||
pairs, err := transfers(cat, src, dst, recursive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.copyAll(cat, pairs, move, func(string, string) {})
|
||||
}
|
||||
for _, bad := range [][2]string{{"a", "d"}, {"a/x", "ab/z"}, {"nope", "d"}, {"", "d"}} {
|
||||
if run(bad[0], bad[1], false, false) == nil {
|
||||
t.Errorf("cp %s %s succeeded", bad[0], bad[1])
|
||||
}
|
||||
}
|
||||
if run("a", "a/b", true, true) == nil {
|
||||
t.Error("moved a folder into itself")
|
||||
}
|
||||
if err := run("a", "d", true, false); err != nil { // cp -r a d
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run("d", "c/", true, true); err != nil { // mv d c/
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run("ab/z", "c", false, true); err != nil { // mv ab/z into existing folder c
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.removeAll(cat, under(cat, "a"), func(string) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fresh, _ := s.catalogue()
|
||||
var names []string
|
||||
for n := range fresh {
|
||||
names = append(names, n)
|
||||
}
|
||||
slices.Sort(names)
|
||||
if want := []string{"c/d/b/y", "c/d/x", "c/w", "c/z"}; !slices.Equal(names, want) {
|
||||
t.Fatalf("names %v, want %v", names, want)
|
||||
}
|
||||
out := filepath.Join(dir, "out")
|
||||
if err := s.get("c/d/x", out); err != nil {
|
||||
t.Fatal("copy lost its data when the original went: ", err)
|
||||
}
|
||||
if b, _ := os.ReadFile(out); string(b) != "a/x" {
|
||||
t.Errorf("got %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_RUNTIME_DIR", dir)
|
||||
t.Setenv(passphraseEnv, "") // restored after the test
|
||||
os.Unsetenv(passphraseEnv) // so keyIdentity asks the agent
|
||||
keyPath := filepath.Join(dir, "key")
|
||||
if err := writeKeyFile(keyPath, "pass", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, err := unlockKeyFile(keyPath, "pass")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := agentIdentity(keyPath); err == nil {
|
||||
t.Fatal("identity from an agent that is not running")
|
||||
}
|
||||
sock, err := agentSocket(keyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret := binary.BigEndian.AppendUint16(nil, id.SuiteID())
|
||||
secret = append(secret, id.Seed()...)
|
||||
pr, pw := io.Pipe()
|
||||
stopped := make(chan struct{})
|
||||
go func() { runAgent(sock, "1m", bytes.NewReader(secret), pw); close(stopped) }()
|
||||
if msg, _ := io.ReadAll(pr); string(msg) != "ok" {
|
||||
t.Fatalf("agent said %q", msg)
|
||||
}
|
||||
got, err := keyIdentity(keyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got.Seed(), id.Seed()) || got.SuiteID() != id.SuiteID() {
|
||||
t.Fatal("agent returned a different key")
|
||||
}
|
||||
if !agentStop(keyPath) {
|
||||
t.Fatal("agent was not running")
|
||||
}
|
||||
<-stopped
|
||||
if _, err := os.Stat(sock); !os.IsNotExist(err) {
|
||||
t.Fatal("socket left behind")
|
||||
}
|
||||
if agentStop(keyPath) {
|
||||
t.Fatal("agent still running")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user