commit c8172d500194a7ae126ab17e3ff1ed0bb38f1014
parent 7f64666c41a9e2f2dd46b154b43c903dca864004
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 1 Oct 2022 11:19:06 -0400
using a kdbx backend now
Diffstat:
20 files changed, 95 insertions(+), 1192 deletions(-)
diff --git a/cmd/main.go b/cmd/main.go
@@ -6,16 +6,12 @@ import (
"errors"
"fmt"
"os"
- "path/filepath"
"strings"
+ "github.com/enckse/lockbox/internal/backend"
"github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/dump"
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/hooks"
"github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/platform"
- "github.com/enckse/lockbox/internal/store"
"github.com/enckse/lockbox/internal/subcommands"
)
@@ -32,25 +28,19 @@ type (
}
)
-func getEntry(fs store.FileSystem, args []string, idx int) string {
+func getEntry(args []string, idx int) string {
if len(args) != idx+1 {
exit("invalid entry given", errors.New("specific entry required"))
}
- return fs.NewPath(args[idx])
+ return args[idx]
}
func internalCallback(name string) callbackFunction {
switch name {
- case "gitdiff":
+ case "diff":
return subcommands.GitDiff
- case "rekey":
- return subcommands.Rekey
- case "rw":
- return subcommands.ReadWrite
case "totp":
return subcommands.TOTP
- case "kdbx":
- return subcommands.ToKeepass
}
return nil
}
@@ -79,22 +69,28 @@ func run() *programError {
if len(args) < 2 {
return newError("missing arguments", errors.New("requires subcommand"))
}
+ t, err := backend.NewTransaction()
+ if err != nil {
+ return newError("unable to build transaction model", err)
+ }
command := args[1]
switch command {
- case "ls", "list", "find":
- opts := subcommands.ListFindOptions{Find: command == "find", Search: "", Store: store.NewFileSystemStore()}
- if opts.Find {
+ case "ls", "find":
+ opts := backend.QueryOptions{}
+ opts.Mode = backend.ListMode
+ if command == "find" {
+ opts.Mode = backend.FindMode
if len(args) < 3 {
return newError("find requires an argument to search for", errors.New("search term required"))
}
- opts.Search = args[2]
+ opts.Criteria = args[2]
}
- files, err := subcommands.ListFindCallback(opts)
+ e, err := t.QueryCallback(opts)
if err != nil {
return newError("unable to list files", err)
}
- for _, f := range files {
- fmt.Println(f)
+ for _, f := range e {
+ fmt.Println(f.Path)
}
case "version":
fmt.Printf("version: %s\n", version)
@@ -115,122 +111,64 @@ func run() *programError {
return newError("too many arguments", errors.New("insert can only perform one operation"))
}
isPipe := inputs.IsInputFromPipe()
- s := store.NewFileSystemStore()
- entry := getEntry(s, args, idx)
- if s.Exists(entry) {
+ entry := getEntry(args, idx)
+ existing, err := t.Get(entry, backend.BlankValue)
+ if err != nil {
+ return newError("unable to find an exact, existing match", err)
+ }
+ if existing != nil {
if !isPipe {
if !confirm("overwrite existing") {
return nil
}
}
- } else {
- dir := filepath.Dir(entry)
- if !s.Exists(dir) {
- if err := os.MkdirAll(dir, 0755); err != nil {
- return newError("failed to create directory structure", err)
- }
- }
}
password, err := inputs.GetUserInputPassword(isPipe, options.Multi)
if err != nil {
return newError("invalid input", err)
}
- if err := encrypt.ToFile(entry, password); err != nil {
- return newError("unable to encrypt object", err)
+ p := strings.TrimSpace(string(password))
+ if err := t.Insert(entry, p, len(strings.Split(p, "\n")) > 1); err != nil {
+ return newError("failed to insert", err)
}
fmt.Println("")
- hooks.Run(hooks.Insert, hooks.PostStep)
- if err := s.GitCommit(entry); err != nil {
- return newError("failed to git commit changed", err)
- }
case "rm":
- s := store.NewFileSystemStore()
- value := args[2]
- var deletes []string
- confirmText := "entry"
- if strings.Contains(value, "*") {
- globs, err := s.Globs(value)
- if err != nil {
- return newError("rm glob failed", err)
- }
- if len(globs) > 1 {
- confirmText = "entries"
- }
- deletes = append(deletes, globs...)
- } else {
- deletes = []string{getEntry(s, args, 2)}
- }
- if len(deletes) == 0 {
- return newError("nothing to delete", errors.New("no files to remove"))
- }
- if confirm(fmt.Sprintf("remove %s", confirmText)) {
- for _, entry := range deletes {
- if !s.Exists(entry) {
- return newError("does not exists", errors.New("can not delete unknown entry"))
- }
- }
- for _, entry := range deletes {
- if err := os.Remove(entry); err != nil {
- return newError("unable to remove entry", err)
- }
- }
- hooks.Run(hooks.Remove, hooks.PostStep)
- if err := s.GitRemove(deletes); err != nil {
- return newError("failed to git remove", err)
- }
- }
- case "show", "clip", "dump":
- fs := store.NewFileSystemStore()
- opts := subcommands.DisplayOptions{Dump: command == "dump", Show: command == "show", Glob: getEntry(fs, []string{"***"}, 0), Store: fs}
- opts.Show = opts.Show || opts.Dump
- startEntry := 2
- options := cli.Arguments{}
- if opts.Dump {
- if len(args) > 2 {
- options = cli.ParseArgs(args[2])
- if options.Yes {
- startEntry = 3
- }
- }
- }
- opts.Entry = getEntry(fs, args, startEntry)
- var err error
- dumpData, err := subcommands.DisplayCallback(opts)
+ deleting := getEntry(args, 2)
+ existing, err := t.Get(deleting, backend.BlankValue)
if err != nil {
- return newError("display command failed to retrieve data", err)
+ return newError("unable to get entity to delete", err)
}
- if opts.Dump {
- if !options.Yes {
- if !confirm("dump data to stdout as plaintext") {
- return nil
- }
+ if confirm("delete entry") {
+ if err := t.Remove(existing); err != nil {
+ return newError("unable to remove entry", err)
}
- d, err := dump.Marshal(dumpData)
- if err != nil {
- return newError("failed to marshal items", err)
- }
- fmt.Println(string(d))
- return nil
+
}
+ case "show", "clip":
+ entry := getEntry(args, 2)
clipboard := platform.Clipboard{}
- if !opts.Show {
+ isShow := command == "show"
+ if isShow {
clipboard, err = platform.NewClipboard()
if err != nil {
return newError("unable to get clipboard", err)
}
}
- for _, obj := range dumpData {
- if opts.Show {
- if obj.Path != "" {
- fmt.Println(obj.Path)
- }
- fmt.Println(obj.Value)
- continue
- }
- if err := clipboard.CopyTo(obj.Value); err != nil {
- return newError("clipboard failed", err)
- }
+ existing, err := t.Get(entry, backend.SecretValue)
+ if err != nil {
+ return newError("unable to get entity", err)
+ }
+ if existing == nil {
+ return newError("entity not found", errors.New("can not find entry"))
}
+ if isShow {
+ fmt.Println(existing.Value)
+ return nil
+ }
+ if err := clipboard.CopyTo(existing.Value); err != nil {
+ return newError("clipboard failed", err)
+ }
+
case "clear":
if err := subcommands.ClearClipboardCallback(); err != nil {
return newError("failed to handle clipboard clear", err)
diff --git a/cmd/vers.txt b/cmd/vers.txt
@@ -1 +1 @@
-v22.09.06
+v22.10.00
diff --git a/contrib/completions.bash b/contrib/completions.bash
@@ -18,15 +18,12 @@ _lb() {
fi
cur=${COMP_WORDS[COMP_CWORD]}
if [ "$COMP_CWORD" -eq 1 ]; then
- opts="version ls show insert rm rekey totp list dump kdbx find$clip_enabled"
+ opts="version ls show insert rm totp find$clip_enabled"
# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
else
if [ "$COMP_CWORD" -eq 2 ]; then
case ${COMP_WORDS[1]} in
- "dump")
- opts="-yes $(lb ls)"
- ;;
"insert")
opts="-multi $(lb ls)"
;;
@@ -36,9 +33,6 @@ _lb() {
opts="$opts -clip"
fi
;;
- "kdbx")
- opts="-file -password"
- ;;
"show" | "rm" | "clip")
opts=$(lb ls)
if [ $(_is_clip "${COMP_WORDS[1]}" "") == 1 ]; then
diff --git a/go.mod b/go.mod
@@ -6,12 +6,12 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/pquerna/otp v1.3.0
github.com/tobischo/gokeepasslib/v3 v3.4.1
- golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
)
require (
github.com/aead/argon2 v0.0.0-20180111183520-a87724528b07 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/boombuler/barcode v1.0.1 // indirect
+ golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
)
diff --git a/internal/dump/export.go b/internal/dump/export.go
@@ -1,19 +0,0 @@
-// Package dump handles export lockbox definitions to other formats.
-package dump
-
-import (
- "encoding/json"
-)
-
-type (
- // ExportEntity represents the output structure from a JSON dump.
- ExportEntity struct {
- Path string `json:"path,omitempty"`
- Value string `json:"value"`
- }
-)
-
-// Marshal handles marshalling of entities to output formats.
-func Marshal(entities []ExportEntity) ([]byte, error) {
- return json.MarshalIndent(entities, "", " ")
-}
diff --git a/internal/encrypt/core.go b/internal/encrypt/core.go
@@ -1,169 +0,0 @@
-// Package encrypt handles encryption/decryption.
-package encrypt
-
-import (
- "crypto/rand"
- "crypto/sha512"
- "errors"
- "io"
- random "math/rand"
- "os"
- "time"
-
- "github.com/enckse/lockbox/internal/inputs"
- "golang.org/x/crypto/nacl/secretbox"
- "golang.org/x/crypto/pbkdf2"
-)
-
-const (
- keyLength = 32
- nonceLength = 24
- padLength = 256
- saltLength = 16
-)
-
-var (
- cryptoMajorVers = uint8(0)
- cryptoMinorVers = uint8(1)
- cryptoVers = []byte{cryptoMajorVers, cryptoMinorVers}
- cryptoVersLength = len(cryptoVers)
- requiredEncryptLength = cryptoVersLength + saltLength + nonceLength
-)
-
-type (
- // Lockbox represents a method to encrypt/decrypt locked files.
- Lockbox struct {
- secret [keyLength]byte
- file string
- }
-
- // LockboxOptions represent options to create a lockbox from.
- LockboxOptions struct {
- Key string
- KeyMode string
- File string
- }
-)
-
-// FromFile decrypts a file-system based encrypted file.
-func FromFile(file string) ([]byte, error) {
- l, err := NewLockbox(LockboxOptions{File: file})
- if err != nil {
- return nil, err
- }
- return l.Decrypt()
-}
-
-// ToFile encrypts data to a file-system based file.
-func ToFile(file string, data []byte) error {
- l, err := NewLockbox(LockboxOptions{File: file})
- if err != nil {
- return err
- }
- return l.Encrypt(data)
-}
-
-// NewLockbox creates a new usable lockbox instance.
-func NewLockbox(options LockboxOptions) (Lockbox, error) {
- return newLockbox(options.Key, options.KeyMode, options.File)
-}
-
-func newLockbox(key, keyMode, file string) (Lockbox, error) {
- b, err := inputs.GetKey(key, keyMode)
- if err != nil {
- return Lockbox{}, err
- }
- var secretKey [keyLength]byte
- copy(secretKey[:], b)
- return Lockbox{secret: secretKey, file: file}, nil
-}
-
-func pad(salt, key []byte) ([keyLength]byte, error) {
- d := pbkdf2.Key(key, salt, 4096, keyLength, sha512.New)
- if len(d) != keyLength {
- return [keyLength]byte{}, errors.New("invalid key result from pad")
- }
- var obj [keyLength]byte
- copy(obj[:], d[:keyLength])
- return obj, nil
-}
-
-func init() {
- random.Seed(time.Now().UnixNano())
-}
-
-// Encrypt will encrypt contents to file.
-func (l Lockbox) Encrypt(datum []byte) error {
- data := datum
- if data == nil {
- b, err := inputs.RawStdin()
- if err != nil {
- return err
- }
- data = b
- }
- if len(data) == 0 {
- return errors.New("no data")
- }
- var padding [padLength]byte
- if _, err := io.ReadFull(rand.Reader, padding[:]); err != nil {
- return err
- }
- padTo := random.Intn(padLength)
- var write []byte
- write = append(write, byte(padTo))
- write = append(write, padding[0:padTo]...)
- write = append(write, data...)
- var salt [saltLength]byte
- if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
- return err
- }
- var nonce [nonceLength]byte
- if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
- return err
- }
- key, err := pad(salt[:], l.secret[:])
- if err != nil {
- return err
- }
- encrypted := secretbox.Seal(nonce[:], write, &nonce, &key)
- var persist []byte
- persist = append(persist, cryptoVers...)
- persist = append(persist, salt[:]...)
- persist = append(persist, encrypted...)
- return os.WriteFile(l.file, persist, 0600)
-}
-
-// Decrypt will decrypt an object from file.
-func (l Lockbox) Decrypt() ([]byte, error) {
- encrypted, err := os.ReadFile(l.file)
- if err != nil {
- return nil, err
- }
- if len(encrypted) <= requiredEncryptLength {
- return nil, errors.New("invalid encrypted data")
- }
- major := encrypted[0]
- minor := encrypted[1]
- if major != cryptoMajorVers || minor != cryptoMinorVers {
- return nil, errors.New("invalid data, bad header")
- }
- var salt [saltLength]byte
- copy(salt[:], encrypted[cryptoVersLength:saltLength+cryptoVersLength])
- key, err := pad(salt[:], l.secret[:])
- if err != nil {
- return nil, err
- }
- var nonce [nonceLength]byte
- copy(nonce[:], encrypted[cryptoVersLength+saltLength:cryptoVersLength+saltLength+nonceLength])
- decrypted, ok := secretbox.Open(nil, encrypted[cryptoVersLength+saltLength+nonceLength:], &nonce, &key)
- if !ok {
- return nil, errors.New("decrypt not ok")
- }
-
- padding := 1 + int(decrypted[0])
- if len(decrypted) < padding {
- return nil, errors.New("invalid decrypted data, bad padding")
- }
- return decrypted[padding:], nil
-}
diff --git a/internal/encrypt/core_test.go b/internal/encrypt/core_test.go
@@ -1,123 +0,0 @@
-package encrypt_test
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/store"
-)
-
-func setupData(t *testing.T) string {
- os.Setenv("LOCKBOX_KEYMODE", "")
- os.Setenv("LOCKBOX_KEY", "")
- if store.NewFileSystemStore().Exists("bin") {
- if err := os.RemoveAll("bin"); err != nil {
- t.Errorf("unable to cleanup dir: %v", err)
- }
- }
-
- if err := os.MkdirAll("bin", 0755); err != nil {
- t.Errorf("failed to setup bin directory: %v", err)
- }
- return filepath.Join("bin", "test.lb")
-}
-
-func TestEncryptDecryptCommand(t *testing.T) {
- e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "echo test", KeyMode: inputs.CommandKeyMode, File: setupData(t)})
- if err != nil {
- t.Errorf("failed to create lockbox: %v", err)
- }
- data := []byte("datum")
- if err := e.Encrypt(data); err != nil {
- t.Errorf("failed to encrypt: %v", err)
- }
- d, err := e.Decrypt()
- if err != nil {
- t.Errorf("failed to encrypt: %v", err)
- }
- if string(d) != string(data) {
- t.Error("data mismatch")
- }
-}
-
-func TestEmptyKey(t *testing.T) {
- setupData(t)
- _, err := encrypt.NewLockbox(encrypt.LockboxOptions{})
- if err == nil || err.Error() != "no key given" {
- t.Errorf("invalid error: %v", err)
- }
- _, err = encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: inputs.CommandKeyMode, Key: "echo"})
- if err == nil || err.Error() != "key is empty" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestKeyLength(t *testing.T) {
- val := ""
- for i := 0; i < 42; i++ {
- val = fmt.Sprintf("a%s", val)
- _, err := encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: inputs.PlainKeyMode, Key: val})
- if err != nil {
- t.Error("no error expected")
- }
- }
-}
-
-func TestUnknownMode(t *testing.T) {
- _, err := encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: "aaa", Key: "echo"})
- if err == nil || err.Error() != "unknown keymode" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestEncryptDecryptPlainText(t *testing.T) {
- e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "plain", KeyMode: inputs.PlainKeyMode, File: setupData(t)})
- if err != nil {
- t.Errorf("failed to create lockbox: %v", err)
- }
- data := []byte("datum")
- if err := e.Encrypt(data); err != nil {
- t.Errorf("failed to encrypt: %v", err)
- }
- d, err := e.Decrypt()
- if err != nil {
- t.Errorf("failed to decrypt: %v", err)
- }
- if string(d) != string(data) {
- t.Error("data mismatch")
- }
-}
-
-func TestEncryptDecryptErrors(t *testing.T) {
- file := setupData(t)
- e, _ := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "plain", KeyMode: inputs.PlainKeyMode, File: file})
- if err := e.Encrypt([]byte{}); err.Error() != "no data" {
- t.Errorf("failed, should be no data: %v", err)
- }
- os.WriteFile(file, []byte{0, 2, 3}, 0600)
- if _, err := e.Decrypt(); err.Error() != "invalid encrypted data" {
- t.Errorf("failed, should be invalid data: %v", err)
- }
- e.Encrypt([]byte("TEST"))
- b, _ := os.ReadFile(file)
- b[0] = 1
- os.WriteFile(file, b, 0600)
- if _, err := e.Decrypt(); err.Error() != "invalid data, bad header" {
- t.Errorf("failed, should be invalid header data: %v", err)
- }
- b[0] = 0
- b[1] = 0
- os.WriteFile(file, b, 0600)
- if _, err := e.Decrypt(); err.Error() != "invalid data, bad header" {
- t.Errorf("failed, should be invalid header data: %v", err)
- }
- b[1] = 1
- os.WriteFile(file, b, 0600)
- if _, err := e.Decrypt(); err != nil {
- t.Error("decrypt should succeed")
- }
-}
diff --git a/internal/hooks/execute.go b/internal/hooks/execute.go
@@ -1,54 +0,0 @@
-// Package hooks handles executing lockbox hooks.
-package hooks
-
-import (
- "errors"
- "os"
- "os/exec"
- "path/filepath"
-
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/store"
-)
-
-type (
- // Action are specific steps that may call a hook.
- Action string
- // Step is the step, during command execution, when the hook was called.
- Step string
-)
-
-const (
- // Remove is called when a store entry is removed.
- Remove Action = "remove"
- // Insert is called when a store entry is inserted.
- Insert Action = "insert"
- // PostStep is a hook running at the end of a command.
- PostStep Step = "post"
-)
-
-// Run executes any configured hooks.
-func Run(action Action, step Step) error {
- hookDir := os.Getenv(inputs.HooksDirEnv)
- if !store.NewFileSystemStore().Exists(hookDir) {
- return nil
- }
- dirs, err := os.ReadDir(hookDir)
- if err != nil {
- return err
- }
- for _, d := range dirs {
- if !d.IsDir() {
- name := d.Name()
- cmd := exec.Command(filepath.Join(hookDir, name), string(action), string(step))
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Run(); err != nil {
- return err
- }
- continue
- }
- return errors.New("hook is not a file")
- }
- return nil
-}
diff --git a/internal/inputs/env.go b/internal/inputs/env.go
@@ -16,16 +16,12 @@ const (
noClipEnv = prefixKey + "NOCLIP"
noColorEnv = prefixKey + "NOCOLOR"
interactiveEnv = prefixKey + "INTERACTIVE"
- gitEnabledEnv = prefixKey + "GIT"
- gitQuietEnv = gitEnabledEnv + "_QUIET"
// TotpEnv allows for overriding of the special name for totp entries.
TotpEnv = prefixKey + "TOTP"
// KeyModeEnv indicates what the KEY value is (e.g. command, plaintext).
KeyModeEnv = prefixKey + "KEYMODE"
// KeyEnv is the key value used by the lockbox store.
KeyEnv = prefixKey + "KEY"
- // HooksDirEnv is the location of hooks to run before/after operations.
- HooksDirEnv = prefixKey + "HOOKDIR"
// PlatformEnv is the platform lb is running on.
PlatformEnv = prefixKey + "PLATFORM"
// StoreEnv is the location of the filesystem store that lb is operating on.
@@ -116,16 +112,6 @@ func IsNoClipEnabled() (bool, error) {
return isYesNoEnv(false, noClipEnv)
}
-// IsGitQuiet indicates if git operations should be 'quiet' (no stdout/stderr)
-func IsGitQuiet() (bool, error) {
- return isYesNoEnv(true, gitQuietEnv)
-}
-
-// IsGitEnabled indicates if the filesystem store is a git repo
-func IsGitEnabled() (bool, error) {
- return isYesNoEnv(true, gitEnabledEnv)
-}
-
// IsNoColorEnabled indicates if the flag is set to disable color.
func IsNoColorEnabled() (bool, error) {
return isYesNoEnv(false, noColorEnv)
diff --git a/internal/store/filesystem.go b/internal/store/filesystem.go
@@ -1,171 +0,0 @@
-// Package store handles filesystem operations for a lockbox store.
-package store
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strings"
-
- "github.com/enckse/lockbox/internal/inputs"
-)
-
-const (
- extension = ".lb"
-)
-
-type (
- // ListEntryFilter allows for filtering/changing view results.
- ListEntryFilter func(string) string
- // FileSystem represents a filesystem store.
- FileSystem struct {
- path string
- }
- // ViewOptions represent list options for parsing store entries.
- ViewOptions struct {
- Display bool
- Filter ListEntryFilter
- ErrorOnEmpty bool
- }
-)
-
-// NewFileSystemStore gets the lockbox directory (filesystem-based) store.
-func NewFileSystemStore() FileSystem {
- return FileSystem{path: os.Getenv(inputs.StoreEnv)}
-}
-
-// Globs will return any globs from the input path from within the store.
-func (s FileSystem) Globs(inputPath string) ([]string, error) {
- return filepath.Glob(filepath.Join(s.path, inputPath))
-}
-
-// List will get all lockbox files in a store.
-func (s FileSystem) List(options ViewOptions) ([]string, error) {
- var results []string
- if !pathExists(s.path) {
- return nil, errors.New("store does not exist")
- }
- err := filepath.Walk(s.path, func(path string, info fs.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if strings.HasSuffix(path, extension) {
- usePath := path
- if options.Display {
- usePath = s.trim(usePath)
- }
- if options.Filter != nil {
- usePath = options.Filter(usePath)
- if usePath == "" {
- return nil
- }
- }
- results = append(results, usePath)
- }
- return nil
- })
-
- if err != nil {
- return nil, err
- }
- if options.ErrorOnEmpty && len(results) == 0 {
- return nil, errors.New("no results found")
- }
- if options.Display {
- sort.Strings(results)
- }
- return results, nil
-}
-
-// NewPath creates a new filesystem store path for an entry.
-func (s FileSystem) NewPath(file string) string {
- return s.NewFile(filepath.Join(s.path, file))
-}
-
-// NewFile creates a new file with the proper extension.
-func (s FileSystem) NewFile(file string) string {
- if !strings.HasSuffix(file, extension) {
- return file + extension
- }
- return file
-}
-
-// CleanPath will clean store and extension information from an entry.
-func (s FileSystem) CleanPath(fullPath string) string {
- return s.trim(fullPath)
-}
-
-func (s FileSystem) trim(path string) string {
- f := strings.TrimPrefix(path, s.path)
- f = strings.TrimPrefix(f, string(os.PathSeparator))
- return strings.TrimSuffix(f, extension)
-}
-
-// Exists will check if a path exists
-func (s FileSystem) Exists(path string) bool {
- return pathExists(path)
-}
-
-// pathExists indicates if a path exists.
-func pathExists(path string) bool {
- if _, err := os.Stat(path); err != nil {
- if os.IsNotExist(err) {
- return false
- }
- }
- return true
-}
-
-// GitCommit is for adding/changing entities
-func (s FileSystem) GitCommit(entry string) error {
- return s.gitAction("add", []string{entry})
-}
-
-// GitRemove is for removing entities
-func (s FileSystem) GitRemove(entries []string) error {
- return s.gitAction("rm", entries)
-}
-
-func (s FileSystem) gitAction(action string, entries []string) error {
- ok, err := inputs.IsGitEnabled()
- if err != nil {
- return err
- }
- if !ok {
- return nil
- }
- if !pathExists(filepath.Join(s.path, ".git")) {
- return nil
- }
- var message []string
- for _, entry := range entries {
- useEntry, err := filepath.Rel(s.path, entry)
- if err != nil {
- return err
- }
- if err := s.gitRun(action, useEntry); err != nil {
- return err
- }
- message = append(message, fmt.Sprintf("lb %s: %s", action, useEntry))
- }
- return s.gitRun("commit", "-m", strings.Join(message, "\n"))
-}
-
-func (s FileSystem) gitRun(args ...string) error {
- arguments := []string{"-C", s.path}
- arguments = append(arguments, args...)
- cmd := exec.Command("git", arguments...)
- ok, err := inputs.IsGitQuiet()
- if err != nil {
- return err
- }
- if !ok {
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- }
- return cmd.Run()
-}
diff --git a/internal/store/filesystem_test.go b/internal/store/filesystem_test.go
@@ -1,123 +0,0 @@
-package store_test
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/store"
-)
-
-func TestListErrors(t *testing.T) {
- os.Setenv(inputs.StoreEnv, "aaa")
- _, err := store.NewFileSystemStore().List(store.ViewOptions{})
- if err == nil || err.Error() != "store does not exist" {
- t.Errorf("invalid store error: %v", err)
- }
-}
-
-func TestList(t *testing.T) {
- testStore := "bin"
- if store.NewFileSystemStore().Exists(testStore) {
- if err := os.RemoveAll(testStore); err != nil {
- t.Errorf("invalid error on remove: %v", err)
- }
- }
- if err := os.MkdirAll(filepath.Join(testStore, "sub"), 0755); err != nil {
- t.Errorf("unable to makedir: %v", err)
- }
- for _, path := range []string{"test", "test2", "aaa", "sub/aaaaajk", "sub/12lkjafav"} {
- if err := os.WriteFile(filepath.Join(testStore, path+".lb"), []byte(""), 0644); err != nil {
- t.Errorf("failed to write %s: %v", path, err)
- }
- }
- os.Setenv(inputs.StoreEnv, testStore)
- s := store.NewFileSystemStore()
- res, err := s.List(store.ViewOptions{})
- if err != nil {
- t.Errorf("unable to list: %v", err)
- }
- if len(res) != 5 {
- t.Error("mismatched results")
- }
- res, err = s.List(store.ViewOptions{Display: true})
- if err != nil {
- t.Errorf("unable to list: %v", err)
- }
- if len(res) != 5 {
- t.Error("mismatched results")
- }
- if res[0] != "aaa" || res[1] != "sub/12lkjafav" || res[2] != "sub/aaaaajk" || res[3] != "test" || res[4] != "test2" {
- t.Errorf("not sorted: %v", res)
- }
- idx := 0
- res, err = s.List(store.ViewOptions{Filter: func(path string) string {
- if strings.Contains(path, "test") {
- idx++
- return fmt.Sprintf("%d", idx)
- }
- return ""
- }})
- if err != nil {
- t.Errorf("unable to list: %v", err)
- }
- if len(res) != 2 || res[0] != "1" || res[1] != "2" {
- t.Error("mismatch filter results")
- }
- res, err = s.List(store.ViewOptions{ErrorOnEmpty: false, Filter: func(path string) string {
- return ""
- }})
- if err != nil {
- t.Errorf("should be non-error: %v", err)
- }
- if len(res) != 0 {
- t.Error("should be empty list")
- }
- _, err = s.List(store.ViewOptions{ErrorOnEmpty: true, Filter: func(path string) string {
- return ""
- }})
- if err == nil || err.Error() != "no results found" {
- t.Errorf("should be non-error: %v", err)
- }
-}
-
-func TestFileSystemFile(t *testing.T) {
- os.Setenv(inputs.StoreEnv, "abc")
- f := store.NewFileSystemStore()
- p := f.NewPath("test")
- if p != "abc/test.lb" {
- t.Error("invalid join result")
- }
-}
-
-func TestCleanPath(t *testing.T) {
- os.Setenv(inputs.StoreEnv, "abc")
- f := store.NewFileSystemStore()
- c := f.CleanPath("xyz")
- if c != "xyz" {
- t.Error("invalid clean")
- }
- c = f.CleanPath("abc/xyz")
- if c != "xyz" {
- t.Error("invalid clean")
- }
- c = f.CleanPath("xyz.lb.lb")
- if c != "xyz.lb" {
- t.Error("invalid clean")
- }
-}
-
-func TestNewFile(t *testing.T) {
- os.Setenv(inputs.StoreEnv, "abc")
- f := store.NewFileSystemStore().NewFile("xyz")
- if f != "xyz.lb" {
- t.Error("invalid file")
- }
- f = store.NewFileSystemStore().NewFile("xyz.lb")
- if f != "xyz.lb" {
- t.Error("invalid file, had suffix")
- }
-}
diff --git a/internal/subcommands/display.go b/internal/subcommands/display.go
@@ -1,79 +0,0 @@
-// Package subcommands handles displaying various lockbox structures to the UI.
-package subcommands
-
-import (
- "errors"
- "fmt"
- "path/filepath"
- "sort"
- "strings"
-
- "github.com/enckse/lockbox/internal/colors"
- "github.com/enckse/lockbox/internal/dump"
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/store"
-)
-
-type (
- // DisplayOptions for getting a set of items for display uses.
- DisplayOptions struct {
- Dump bool
- Entry string
- Show bool
- Glob string
- All bool
- Store store.FileSystem
- }
-)
-
-// DisplayCallback handles getting entries for display.
-func DisplayCallback(args DisplayOptions) ([]dump.ExportEntity, error) {
- entries := []string{args.Entry}
- if strings.Contains(args.Entry, "*") || args.All {
- if args.Entry == args.Glob || args.All {
- all, err := args.Store.List(store.ViewOptions{})
- if err != nil {
- return nil, err
- }
- entries = all
- } else {
- matches, err := filepath.Glob(args.Entry)
- if err != nil {
- return nil, err
- }
- entries = matches
- }
- }
- isGlob := len(entries) > 1
- if isGlob {
- if !args.Show {
- return nil, errors.New("bad glob request")
- }
- sort.Strings(entries)
- }
- coloring, err := colors.NewTerminal(colors.Red)
- if err != nil {
- return nil, err
- }
- dumpData := []dump.ExportEntity{}
- for _, entry := range entries {
- if !args.Store.Exists(entry) {
- return nil, errors.New("entry not found")
- }
- decrypt, err := encrypt.FromFile(entry)
- if err != nil {
- return nil, err
- }
- entity := dump.ExportEntity{Value: strings.TrimSpace(string(decrypt))}
- if args.Show && isGlob {
- fileName := args.Store.CleanPath(entry)
- if args.Dump {
- entity.Path = fileName
- } else {
- entity.Path = fmt.Sprintf("%s%s:%s", coloring.Start, fileName, coloring.End)
- }
- }
- dumpData = append(dumpData, entity)
- }
- return dumpData, nil
-}
diff --git a/internal/subcommands/gitdiff.go b/internal/subcommands/gitdiff.go
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
- "github.com/enckse/lockbox/internal/encrypt"
+ "github.com/enckse/lockbox/internal/backend"
)
// GitDiff handles git diffing of lb entries.
@@ -13,12 +13,16 @@ func GitDiff(args []string) error {
if len(args) == 0 {
return errors.New("git diff requires a file")
}
- result, err := encrypt.FromFile(args[len(args)-1])
+ t, err := backend.Load(args[len(args)-1])
if err != nil {
return err
}
- if result != nil {
- fmt.Println(string(result))
+ e, err := t.QueryCallback(backend.QueryOptions{Mode: backend.ListMode, Values: backend.HashedValue})
+ if err != nil {
+ return err
+ }
+ for _, item := range e {
+ fmt.Printf("%s:\nhash:%s\n", item.Path, item.Value)
}
return nil
}
diff --git a/internal/subcommands/kdbx.go b/internal/subcommands/kdbx.go
@@ -1,92 +0,0 @@
-package subcommands
-
-import (
- "errors"
- "flag"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/store"
- "github.com/tobischo/gokeepasslib/v3"
- "github.com/tobischo/gokeepasslib/v3/wrappers"
-)
-
-func value(key string, value string) gokeepasslib.ValueData {
- return gokeepasslib.ValueData{Key: key, Value: gokeepasslib.V{Content: value}}
-}
-
-func protectedValue(key string, value string) gokeepasslib.ValueData {
- return gokeepasslib.ValueData{
- Key: key,
- Value: gokeepasslib.V{Content: value, Protected: wrappers.NewBoolWrapper(true)},
- }
-}
-
-// ToKeepass converts the lb store to a kdbx file.
-func ToKeepass(args []string) error {
- flags := flag.NewFlagSet("kdbx", flag.ExitOnError)
- file := flags.String("file", "", "file to write to")
- pass := flags.String("password", "", "password to use for the kdbx output (default is lb store key)")
- if err := flags.Parse(args); err != nil {
- return err
- }
- fileName := *file
- if fileName == "" {
- return errors.New("no file given")
- }
- key := *pass
- if strings.TrimSpace(key) == "" {
- v, err := inputs.GetKey("", "")
- if err != nil {
- return err
- }
- key = string(v)
- }
- entries, err := DisplayCallback(DisplayOptions{All: true, Dump: true, Show: true, Store: store.NewFileSystemStore()})
- if err != nil {
- return err
- }
- if len(entries) == 0 {
- return errors.New("nothing to convert")
- }
- root := gokeepasslib.NewGroup()
- root.Name = "root"
- count := 0
- for _, entry := range entries {
- e := gokeepasslib.NewEntry()
- path := entry.Path
- val := entry.Value
- e.Values = append(e.Values, value("Title", filepath.Dir(path)))
- e.Values = append(e.Values, value("UserName", filepath.Base(path)))
- field := "Password"
- if len(strings.Split(strings.TrimSpace(val), "\n")) > 1 {
- field = "Notes"
- }
- e.Values = append(e.Values, protectedValue(field, val))
- root.Entries = append(root.Entries, e)
- count++
- }
- db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4())
- db.Credentials = gokeepasslib.NewPasswordCredentials(key)
- db.Content.Root =
- &gokeepasslib.RootData{
- Groups: []gokeepasslib.Group{root},
- }
- if err := db.LockProtectedEntries(); err != nil {
- return err
- }
- f, err := os.Create(fileName)
- if err != nil {
- return err
- }
- defer f.Close()
- encoder := gokeepasslib.NewEncoder(f)
- if err := encoder.Encode(db); err != nil {
- return err
- }
- fmt.Printf("exported %d entries to %s\n", count, fileName)
- return nil
-}
diff --git a/internal/subcommands/listfind.go b/internal/subcommands/listfind.go
@@ -1,35 +0,0 @@
-// Package subcommands handles listing items from the lockbox store.
-package subcommands
-
-import (
- "strings"
-
- "github.com/enckse/lockbox/internal/store"
-)
-
-type (
- // ListFindOptions for listing/finding entries in a store.
- ListFindOptions struct {
- Find bool
- Search string
- Store store.FileSystem
- }
-)
-
-// ListFindCallback for searching/finding/listing entries.
-func ListFindCallback(args ListFindOptions) ([]string, error) {
- viewOptions := store.ViewOptions{Display: true}
- if args.Find {
- viewOptions.Filter = func(inPath string) string {
- if strings.Contains(inPath, args.Search) {
- return inPath
- }
- return ""
- }
- }
- files, err := args.Store.List(viewOptions)
- if err != nil {
- return nil, err
- }
- return files, nil
-}
diff --git a/internal/subcommands/readwrite.go b/internal/subcommands/readwrite.go
@@ -1,42 +0,0 @@
-// Package subcommands perform a read/write against a specific lockbox object.
-package subcommands
-
-import (
- "errors"
- "flag"
- "fmt"
-
- "github.com/enckse/lockbox/internal/encrypt"
-)
-
-// ReadWrite performs singular read/write encryption operations.
-func ReadWrite(args []string) error {
- flags := flag.NewFlagSet("readwrite", flag.ExitOnError)
- mode := flags.String("mode", "", "decrypt/encrypt")
- key := flags.String("key", "", "security key")
- file := flags.String("file", "", "file to process")
- keyMode := flags.String("keymode", "", "key lookup mode")
- if err := flags.Parse(args); err != nil {
- return err
- }
-
- l, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: *key, KeyMode: *keyMode, File: *file})
- if err != nil {
- return err
- }
- switch *mode {
- case "encrypt":
- if err := l.Encrypt(nil); err != nil {
- return err
- }
- case "decrypt":
- results, err := l.Decrypt()
- if err != nil {
- return err
- }
- fmt.Println(string(results))
- default:
- return errors.New("invalid read/write modeE")
- }
- return nil
-}
diff --git a/internal/subcommands/rekey.go b/internal/subcommands/rekey.go
@@ -1,50 +0,0 @@
-// Package subcommands handles rekeying.
-package subcommands
-
-import (
- "flag"
- "fmt"
- "strings"
-
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/store"
-)
-
-// Rekey handles rekeying a lockbox entirely.
-func Rekey(args []string) error {
- flags := flag.NewFlagSet("rekey", flag.ExitOnError)
- inKey := flags.String("inkey", "", "input encryption key to read current values")
- outKey := flags.String("outkey", "", "output encryption key to update values with")
- inMode := flags.String("inmode", "", "input encryption key mode")
- outMode := flags.String("outmode", "", "output encryption key mode")
- if err := flags.Parse(args); err != nil {
- return err
- }
- found, err := store.NewFileSystemStore().List(store.ViewOptions{})
- if err != nil {
- return err
- }
- inOpts := encrypt.LockboxOptions{Key: *inKey, KeyMode: *inMode}
- outOpts := encrypt.LockboxOptions{Key: *outKey, KeyMode: *outMode}
- for _, file := range found {
- fmt.Printf("rekeying: %s\n", file)
- inOpts.File = file
- in, err := encrypt.NewLockbox(inOpts)
- if err != nil {
- return err
- }
- decrypt, err := in.Decrypt()
- if err != nil {
- return err
- }
- outOpts.File = file
- out, err := encrypt.NewLockbox(outOpts)
- if err != nil {
- return err
- }
- if err := out.Encrypt([]byte(strings.TrimSpace(string(decrypt)))); err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/internal/subcommands/totp.go b/internal/subcommands/totp.go
@@ -6,18 +6,15 @@ import (
"fmt"
"os"
"os/exec"
- "path/filepath"
- "sort"
"strconv"
"strings"
"time"
+ "github.com/enckse/lockbox/internal/backend"
"github.com/enckse/lockbox/internal/cli"
"github.com/enckse/lockbox/internal/colors"
- "github.com/enckse/lockbox/internal/encrypt"
"github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/platform"
- "github.com/enckse/lockbox/internal/store"
otp "github.com/pquerna/otp/totp"
)
@@ -92,17 +89,18 @@ func display(token string, args cli.Arguments) error {
if err != nil {
return err
}
- f := store.NewFileSystemStore()
- tok := filepath.Join(strings.TrimSpace(token), totpEnv())
- pathing := f.NewPath(tok)
- if !f.Exists(pathing) {
- return errors.New("object does not exist")
+ t, err := backend.NewTransaction()
+ if err != nil {
+ return err
}
- val, err := encrypt.FromFile(pathing)
+ entity, err := t.Get(token, backend.SecretValue)
if err != nil {
return err
}
- totpToken := string(val)
+ if entity == nil {
+ return errors.New("object does not exist")
+ }
+ totpToken := string(entity.Value)
if !interactive {
code, err := otp.GenerateCode(totpToken, time.Now())
if err != nil {
@@ -166,7 +164,7 @@ func display(token string, args cli.Arguments) error {
expires := fmt.Sprintf("%s%s (%s)%s", startColor, now.Format("15:04:05"), leftString, endColor)
outputs := []string{expires}
if !args.Clip {
- outputs = append(outputs, fmt.Sprintf("%s\n %s", tok, code))
+ outputs = append(outputs, fmt.Sprintf("%s\n %s", token, code))
if !args.Once {
outputs = append(outputs, "-> CTRL+C to exit")
}
@@ -192,20 +190,16 @@ func TOTP(args []string) error {
cmd := args[0]
options := cli.ParseArgs(cmd)
if options.List {
- f := store.NewFileSystemStore()
- token := f.NewFile(totpEnv())
- results, err := f.List(store.ViewOptions{ErrorOnEmpty: true, Filter: func(path string) string {
- if filepath.Base(path) == token {
- return filepath.Dir(f.CleanPath(path))
- }
- return ""
- }})
+ t, err := backend.NewTransaction()
+ if err != nil {
+ return err
+ }
+ e, err := t.QueryCallback(backend.QueryOptions{Mode: backend.SuffixMode, Criteria: fmt.Sprintf("%c%s", os.PathSeparator, totpEnv())})
if err != nil {
return err
}
- sort.Strings(results)
- for _, entry := range results {
- fmt.Println(entry)
+ for _, entry := range e {
+ fmt.Println(entry.Path)
}
return nil
}
diff --git a/tests/expected.log b/tests/expected.log
@@ -1,32 +1,10 @@
-[]
-HOOK RAN insert post
-HOOK RAN insert post
-keys/one:
-test
-keys/one2:
-test2
-[
- {
- "path": "keys/one",
- "value": "test"
- },
- {
- "path": "keys/one2",
- "value": "test2"
- }
-]
-HOOK RAN insert post
keys/one
keys/one2
keys2/three
-rekeying: /keys/one.lb
-rekeying: /keys/one2.lb
-rekeying: /keys2/three.lb
-remove entry? (y/N) HOOK RAN remove post
-
+delete entry? (y/N)
keys/one2
keys2/three
keys/one2
@@ -34,21 +12,14 @@ keys2/three
test2
test3
test4
-dump data to stdout as plaintext? (y/N) [
- {
- "value": "test3\ntest4"
- }
-]
-
-HOOK RAN insert post
-test
-XXXXXX
-test2
-remove entry? (y/N) HOOK RAN remove post
-
-remove entry? (y/N) HOOK RAN remove post
-exported 1 entries to bin/file.kdbx
-display command failed to retrieve data (decrypt not ok)
-rekeying: /keys/one2.lb
-test2
+test/totp
+totp command failure (object does not exist)
+keys/one2:
+hash:7465737432cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
+keys2/three:
+hash:74657374330a7465737434cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
+test/totp:
+hash:35616534373261627164656b6a71796b6f79786b37687663326c656b6c71356ecf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
+delete entry? (y/N)
+delete entry? (y/N)
+\ No newline at end of file
diff --git a/tests/run.sh b/tests/run.sh
@@ -2,61 +2,33 @@
BIN="$1"
TESTS="$2"
-export LOCKBOX_STORE="$TESTS/lb"
+export LOCKBOX_STORE="$TESTS/lb.kdbx"
export LOCKBOX_KEYMODE="plaintext"
export LOCKBOX_KEY="plaintextkey"
export LOCKBOX_TOTP="totp"
export LOCKBOX_INTERACTIVE="no"
-export LOCKBOX_HOOKDIR="$TESTS/hooks"
-export LOCKBOX_GIT="no"
rm -rf $TESTS
-mkdir -p $LOCKBOX_STORE
-mkdir -p $LOCKBOX_STORE/$LOCKBOX_TOTP
-git -C $LOCKBOX_STORE init
-echo "TEST" > $LOCKBOX_STORE/init
-git -C $LOCKBOX_STORE add .
-git -C $LOCKBOX_STORE config user.email "you@example.com"
-git -C $LOCKBOX_STORE config user.name "Your Name"
-git -C $LOCKBOX_STORE commit -am "init"
-HOOK=$LOCKBOX_HOOKDIR/hook
-mkdir -p $LOCKBOX_HOOKDIR
-
-_hook() {
- echo "#!/bin/sh"
- echo "echo HOOK RAN \$@"
-}
+mkdir -p $TESTS
_run() {
- echo "y" | "$BIN/lb" dump -yes "*"
echo "test" | "$BIN/lb" insert keys/one
echo "test2" | "$BIN/lb" insert keys/one2
- "$BIN/lb" show keys/*
- "$BIN/lb" dump -yes '***'
echo -e "test3\ntest4" | "$BIN/lb" insert keys2/three
"$BIN/lb" ls
- "$BIN/lb" "rekey"
yes 2>/dev/null | "$BIN/lb" rm keys/one
echo
- "$BIN/lb" list
+ "$BIN/lb" ls
"$BIN/lb" find e
"$BIN/lb" show keys/one2
"$BIN/lb" show keys2/three
- echo "y" | "$BIN/lb" dump keys2/three
echo "5ae472abqdekjqykoyxk7hvc2leklq5n" | "$BIN/lb" insert test/totp
"$BIN/lb" "totp" -list
"$BIN/lb" "totp" test | tr '[:digit:]' 'X'
- "$BIN/lb" "gitdiff" bin/lb/keys/one.lb bin/lb/keys/one2.lb
+ "$BIN/lb" "diff" $LOCKBOX_STORE
yes 2>/dev/null | "$BIN/lb" rm keys2/three
echo
yes 2>/dev/null | "$BIN/lb" rm test/totp
- echo
- "$BIN/lb" kdbx -file bin/file.kdbx
- LOCKBOX_KEY="invalid" "$BIN/lb" show keys/one2
- "$BIN/lb" "rekey" -outkey "test" -outmode "plaintext"
- "$BIN/lb" rw -file bin/lb/keys/one2.lb -key "test" -keymode "plaintext" -mode "decrypt"
}
-_hook > $HOOK
-chmod 755 $HOOK
_run 2>&1 | sed "s#$LOCKBOX_STORE##g" > $TESTS/actual.log