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:
Andy Dixon
2026-10-02 22:13:06 +01:00
parent 0d45ecea59
commit ec204f0ea2
31 changed files with 807 additions and 51 deletions
+24 -15
View File
@@ -586,23 +586,28 @@ func (s *store) removeAll(cat map[string][]*record, names []string, done func(na
return nil
}
func (s *store) rename(oldName, newName string) error {
if !validName(newName) {
return fmt.Errorf("invalid vault path %q", newName)
// copyAll stores each pair's first name again under its second, sharing its
// objects, then with move deletes the first names. New records go first, so
// an interruption leaves a file under both names, never neither.
func (s *store) copyAll(cat map[string][]*record, pairs [][2]string, move bool, done func(from, to string)) error {
var old []string
for _, p := range pairs {
r := *cat[p[0]][0]
r.Name = p[1]
if !move {
r.Added = time.Now().UTC()
}
if err := s.writeRecord(&r); err != nil {
return err
}
cat[p[1]] = []*record{&r}
old = append(old, p[0])
done(p[0], p[1])
}
cat, r, err := s.lookup(oldName)
if err != nil {
return err
if !move {
return nil
}
if len(cat[newName]) > 0 {
return fmt.Errorf("%s already exists", newName)
}
moved := *r
moved.Name = newName
if err := s.writeRecord(&moved); err != nil {
return err
}
return s.remove(cat, oldName, false)
return s.removeAll(cat, old, func(string) {})
}
// get decrypts name to out. Nothing appears at out until every object,
@@ -612,6 +617,10 @@ func (s *store) get(name, out string) error {
if err != nil {
return err
}
return s.getRecord(r, out)
}
func (s *store) getRecord(r *record, out string) error {
if _, err := os.Lstat(out); err == nil {
return fmt.Errorf("%s already exists", out)
}