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:
@@ -46,16 +46,27 @@ const usage = `Usage:
|
||||
vault list [FOLDER] list every stored file under FOLDER,
|
||||
with full names
|
||||
vault get NAME [-o OUTPUT] decrypt a stored file (default: its base name)
|
||||
vault mv OLD NEW rename a stored file
|
||||
vault get -r FOLDER [-o DIR] decrypt a folder and everything under it
|
||||
into DIR (default: the folder's base name)
|
||||
vault mv OLD NEW rename or move a file or folder; NEW
|
||||
ending in / or naming an existing folder
|
||||
moves OLD into it
|
||||
vault cp [-r] OLD NEW copy a file, or with -r a folder, like mv;
|
||||
copies share their encrypted data
|
||||
vault rm NAME delete a stored file
|
||||
vault rm -r FOLDER [-f] delete a folder and everything under it;
|
||||
asks first unless -f (or -rf) is given
|
||||
vault unlock [--for DURATION] remember the passphrase for DURATION
|
||||
(default 5m), so commands skip asking
|
||||
vault lock forget it now
|
||||
|
||||
Options (any command):
|
||||
--key PATH key file (default $VAULT_KEY, else ~/.vault/key)
|
||||
--vault DIR vault directory (default $VAULT_DIR, else ~/.vault/store)
|
||||
|
||||
The passphrase is read from the terminal, or from $VAULT_PASSPHRASE.
|
||||
The passphrase is read from the terminal, or from $VAULT_PASSPHRASE. After
|
||||
vault unlock, commands use the remembered key instead, unless
|
||||
$VAULT_PASSPHRASE is set.
|
||||
|
||||
Decoy passphrase: genkey offers a second, optional passphrase
|
||||
($VAULT_DECOY_PASSPHRASE when scripted). It unlocks a separate key, so
|
||||
@@ -80,6 +91,10 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
cmd, args := os.Args[1], os.Args[2:]
|
||||
if cmd == agentCmd && len(args) == 2 {
|
||||
runAgent(args[0], args[1], os.Stdin, os.Stdout)
|
||||
return
|
||||
}
|
||||
fset := flag.NewFlagSet("vault "+cmd, flag.ContinueOnError)
|
||||
fset.SetOutput(io.Discard)
|
||||
keyPath := fset.String("key", defaultPath("VAULT_KEY", "key"), "")
|
||||
@@ -87,6 +102,7 @@ func main() {
|
||||
output := fset.String("o", "", "")
|
||||
filler := fset.String("filler", "0", "")
|
||||
yes := fset.Bool("yes", false, "")
|
||||
ttl := fset.Duration("for", 5*time.Minute, "")
|
||||
// rm's -r and -f; Go's flag package has no combined short flags, so
|
||||
// -rf and -fr are spelled out.
|
||||
recurse, force := fset.Bool("r", false, ""), fset.Bool("f", false, "")
|
||||
@@ -113,6 +129,7 @@ func main() {
|
||||
fail("decoy passphrase must differ from the passphrase")
|
||||
}
|
||||
check(writeKeyFile(*keyPath, pass, decoy))
|
||||
agentStop(*keyPath) // one left from an old key at this path would serve it
|
||||
fmt.Fprintf(os.Stderr, "key written to %s\n", *keyPath)
|
||||
case "init":
|
||||
want(0, 0)
|
||||
@@ -198,12 +215,40 @@ func main() {
|
||||
want(1, 1)
|
||||
out := *output
|
||||
if out == "" {
|
||||
out = path.Base(pos[0])
|
||||
out = path.Base(strings.TrimRight(pos[0], "/"))
|
||||
}
|
||||
check(open(*keyPath, *vaultDir, false).get(pos[0], out))
|
||||
case "mv", "rename":
|
||||
s := open(*keyPath, *vaultDir, false)
|
||||
if !*recurse {
|
||||
check(s.get(pos[0], out))
|
||||
break
|
||||
}
|
||||
cat, err := s.catalogue()
|
||||
check(err)
|
||||
folder := strings.Trim(pos[0], "/")
|
||||
names := under(cat, folder)
|
||||
if folder == "" || len(names) == 0 || len(cat[folder]) > 0 {
|
||||
fail("%s: not a folder in the vault", pos[0])
|
||||
}
|
||||
for _, n := range names {
|
||||
dst := filepath.Join(out, filepath.FromSlash(n[len(folder)+1:]))
|
||||
check(os.MkdirAll(filepath.Dir(dst), 0o700))
|
||||
check(s.getRecord(cat[n][0], dst))
|
||||
fmt.Fprintf(os.Stderr, "extracted %s\n", dst)
|
||||
}
|
||||
case "mv", "rename", "cp":
|
||||
want(2, 2)
|
||||
check(open(*keyPath, *vaultDir, true).rename(pos[0], pos[1]))
|
||||
s := open(*keyPath, *vaultDir, true)
|
||||
cat, err := s.catalogue()
|
||||
check(err)
|
||||
pairs, err := transfers(cat, pos[0], pos[1], cmd != "cp" || *recurse)
|
||||
check(err)
|
||||
verb := "copied"
|
||||
if cmd != "cp" {
|
||||
verb = "moved"
|
||||
}
|
||||
check(s.copyAll(cat, pairs, cmd != "cp", func(from, to string) {
|
||||
fmt.Fprintf(os.Stderr, "%s %s -> %s\n", verb, from, to)
|
||||
}))
|
||||
case "rm", "delete":
|
||||
want(1, 1)
|
||||
recursive, forced := *recurse || *rf || *fr, *force || *rf || *fr
|
||||
@@ -241,6 +286,26 @@ func main() {
|
||||
if recursive {
|
||||
fmt.Fprintf(os.Stderr, "removed %d file(s), %s\n", len(names), humanSize(size))
|
||||
}
|
||||
case "unlock":
|
||||
want(0, 0)
|
||||
if *ttl <= 0 {
|
||||
fail("--for needs a positive duration, such as 5m or 1h")
|
||||
}
|
||||
pass, err := term.ReadPassphrase("Enter passphrase:", passphraseEnv)
|
||||
check(err)
|
||||
id, err := unlockKeyFile(*keyPath, pass)
|
||||
check(err)
|
||||
check(agentStart(*keyPath, id, *ttl))
|
||||
id.Close()
|
||||
fmt.Fprintf(os.Stderr, "unlocked until %s; vault lock forgets the key sooner\n",
|
||||
time.Now().Add(*ttl).Format("15:04:05"))
|
||||
case "lock":
|
||||
want(0, 0)
|
||||
if agentStop(*keyPath) {
|
||||
fmt.Fprintln(os.Stderr, "locked")
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "nothing was unlocked")
|
||||
}
|
||||
case "help", "-h", "--help":
|
||||
fmt.Println(usage)
|
||||
default:
|
||||
@@ -248,6 +313,51 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// transfers works out, like mv(1) and cp(1), which stored names src and dst
|
||||
// mean: each file to copy or move, paired with its new name. src is a file,
|
||||
// or with recursive a folder. dst ending in / or naming an existing folder
|
||||
// means into it. No new name may already exist.
|
||||
func transfers(cat map[string][]*record, src, dst string, recursive bool) ([][2]string, error) {
|
||||
into := dst == "" || strings.HasSuffix(dst, "/")
|
||||
src, dst = strings.Trim(src, "/"), strings.Trim(dst, "/")
|
||||
if src == "" {
|
||||
return nil, errors.New("name a file or folder to copy or move")
|
||||
}
|
||||
if dst == "" || len(cat[dst]) == 0 && len(under(cat, dst)) > 0 {
|
||||
into = true
|
||||
}
|
||||
if into {
|
||||
dst = path.Join(dst, path.Base(src))
|
||||
}
|
||||
var pairs [][2]string
|
||||
if len(cat[src]) > 0 {
|
||||
pairs = [][2]string{{src, dst}}
|
||||
} else {
|
||||
names := under(cat, src)
|
||||
if len(names) == 0 {
|
||||
return nil, fmt.Errorf("%s: not in the vault", src)
|
||||
}
|
||||
if !recursive {
|
||||
return nil, fmt.Errorf("%s is a folder; use -r", src)
|
||||
}
|
||||
if dst == src || strings.HasPrefix(dst, src+"/") {
|
||||
return nil, fmt.Errorf("cannot put %s inside itself", src)
|
||||
}
|
||||
for _, n := range names {
|
||||
pairs = append(pairs, [2]string{n, dst + n[len(src):]})
|
||||
}
|
||||
}
|
||||
for _, p := range pairs {
|
||||
if !validName(p[1]) {
|
||||
return nil, fmt.Errorf("invalid vault path %q", p[1])
|
||||
}
|
||||
if len(cat[p[1]]) > 0 {
|
||||
return nil, fmt.Errorf("%s already exists", p[1])
|
||||
}
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// parseInterleaved lets flags appear before, between or after positional
|
||||
// arguments.
|
||||
func parseInterleaved(fset *flag.FlagSet, args []string) ([]string, error) {
|
||||
@@ -294,9 +404,7 @@ func defaultPath(env, name string) string {
|
||||
// open unlocks the key and opens the vault, holding the vault lock
|
||||
// exclusively for writers and shared for readers.
|
||||
func open(keyPath, vaultDir string, exclusive bool) *store {
|
||||
pass, err := term.ReadPassphrase("Enter passphrase:", passphraseEnv)
|
||||
check(err)
|
||||
id, err := unlockKeyFile(keyPath, pass)
|
||||
id, err := keyIdentity(keyPath)
|
||||
check(err)
|
||||
s, err := openStore(vaultDir, id)
|
||||
check(err)
|
||||
|
||||
Reference in New Issue
Block a user