// Copyright 2026 The vault Authors. // SPDX-License-Identifier: GPL-3.0-or-later // vault is a post-quantum encrypted file vault. package main import ( "flag" "fmt" "io" "io/fs" "os" "path" "path/filepath" "sort" "strings" "github.com/andydixon/vault/internal/term" ) const usage = `Usage: vault genkey create a new passphrase-protected key vault init create a new, empty vault vault add FILE|DIR [FOLDER] encrypt a file or directory into the vault, optionally under a folder inside the vault vault list [PREFIX] list stored files vault get NAME [-o OUTPUT] decrypt a stored file (default: its base name) vault mv OLD NEW rename a stored file vault rm NAME delete a stored file 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.` const passphraseEnv = "VAULT_PASSPHRASE" func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, usage) os.Exit(2) } cmd, args := os.Args[1], os.Args[2:] fset := flag.NewFlagSet("vault "+cmd, flag.ContinueOnError) fset.SetOutput(io.Discard) keyPath := fset.String("key", defaultPath("VAULT_KEY", "key"), "") vaultDir := fset.String("vault", defaultPath("VAULT_DIR", "store"), "") output := fset.String("o", "", "") pos, err := parseInterleaved(fset, args) if err != nil { fail("%v\n\n%s", err, usage) } want := func(min, max int) { if len(pos) < min || len(pos) > max { fail("wrong number of arguments for %s\n\n%s", cmd, usage) } } switch cmd { case "genkey": want(0, 0) if _, err := os.Stat(*keyPath); err == nil { fail("%s already exists", *keyPath) } pass := newPassphrase("passphrase") check(writeKeyFile(*keyPath, pass)) fmt.Fprintf(os.Stderr, "key written to %s\n", *keyPath) case "init": want(0, 0) check(initStore(*vaultDir)) fmt.Fprintf(os.Stderr, "vault created at %s\n", *vaultDir) case "add": want(1, 2) dest := "" if len(pos) == 2 { dest = strings.Trim(pos[1], "/") } s := open(*keyPath, *vaultDir) cat, err := s.catalogue() check(err) check(addPath(s, cat, pos[0], dest)) case "list", "ls": want(0, 1) prefix := "" if len(pos) == 1 { prefix = strings.Trim(pos[0], "/") } cat, err := open(*keyPath, *vaultDir).catalogue() check(err) list(cat, prefix) case "get": want(1, 1) out := *output if out == "" { out = path.Base(pos[0]) } check(open(*keyPath, *vaultDir).get(pos[0], out)) case "mv", "rename": want(2, 2) check(open(*keyPath, *vaultDir).rename(pos[0], pos[1])) case "rm", "delete": want(1, 1) check(open(*keyPath, *vaultDir).delete(pos[0])) case "help", "-h", "--help": fmt.Println(usage) default: fail("unknown command %q\n\n%s", cmd, usage) } } // parseInterleaved lets flags appear before, between or after positional // arguments. func parseInterleaved(fset *flag.FlagSet, args []string) ([]string, error) { var pos []string for { if err := fset.Parse(args); err != nil { return nil, err } if fset.NArg() == 0 { return pos, nil } pos = append(pos, fset.Arg(0)) args = fset.Args()[1:] } } func defaultPath(env, name string) string { if v := os.Getenv(env); v != "" { return v } home, err := os.UserHomeDir() if err != nil { fail("cannot find home directory; set %s", env) } return filepath.Join(home, ".vault", name) } func open(keyPath, vaultDir string) *store { pass, err := term.ReadPassphrase("Enter passphrase:", passphraseEnv) check(err) id, err := unlockKeyFile(keyPath, pass) check(err) s, err := openStore(vaultDir, id) check(err) return s } func newPassphrase(what string) string { if p, ok := os.LookupEnv(passphraseEnv); ok && p != "" { return p } p, err := term.ReadPassphrase("Enter "+what+":", passphraseEnv) check(err) if p == "" { fail("%s must not be empty", what) } again, err := term.ReadPassphrase("Confirm "+what+":", passphraseEnv) check(err) if p != again { fail("%ss do not match", what) } return p } // addPath adds a file, or every regular file under a directory, beneath dest. func addPath(s *store, cat map[string][]*record, src, dest string) error { info, err := os.Stat(src) if err != nil { return err } base := filepath.Base(filepath.Clean(src)) if !info.IsDir() { return addOne(s, cat, src, path.Join(dest, base)) } return filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } if !d.Type().IsRegular() { fmt.Fprintf(os.Stderr, "skipping %s: not a regular file\n", p) return nil } rel, err := filepath.Rel(src, p) if err != nil { return err } return addOne(s, cat, p, path.Join(dest, base, filepath.ToSlash(rel))) }) } func addOne(s *store, cat map[string][]*record, src, name string) error { replaced, err := s.add(cat, src, name) if err != nil { return err } verb := "added" if replaced { verb = "replaced" } fmt.Fprintf(os.Stderr, "%s %s\n", verb, name) return nil } func list(cat map[string][]*record, prefix string) { var names []string for name := range cat { if prefix == "" || name == prefix || strings.HasPrefix(name, prefix+"/") { names = append(names, name) } } sort.Strings(names) for _, name := range names { r := cat[name][0] fmt.Printf("%10s %s %s\n", humanSize(r.Size), r.Modified.Local().Format("2006-01-02 15:04"), name) } } func humanSize(n int64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%d B", n) } div, exp := int64(unit), 0 for m := n / unit; m >= unit; m /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp]) } func check(err error) { if err != nil { fail("%v", err) } } func fail(format string, args ...any) { fmt.Fprintf(os.Stderr, "vault: "+format+"\n", args...) os.Exit(1) }