commit 036116b8955a9120c4fd323d99ce63076d23a46e
parent 5d79caf3c018b6659e92e3a8030147a85f5f88b7
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 17 Sep 2022 10:26:19 -0400
adding kdbx conversion helper
Diffstat:
9 files changed, 177 insertions(+), 56 deletions(-)
diff --git a/cmd/main.go b/cmd/main.go
@@ -47,6 +47,8 @@ func internalCallback(name string) callbackFunction {
return subcommands.ReadWrite
case "totp":
return subcommands.TOTP
+ case "kdbx":
+ return subcommands.ToKeepass
}
return nil
}
diff --git a/contrib/completions.bash b/contrib/completions.bash
@@ -18,7 +18,7 @@ _lb() {
fi
cur=${COMP_WORDS[COMP_CWORD]}
if [ "$COMP_CWORD" -eq 1 ]; then
- opts="version ls show insert rm rekey totp list dump find$clip_enabled"
+ opts="version ls show insert rm rekey totp list dump kdbx find$clip_enabled"
# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
else
@@ -36,6 +36,9 @@ _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
@@ -5,10 +5,13 @@ go 1.18
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-20220829220503-c86fa9a7ed90
)
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/sys v0.0.0-20220913120320-3275c407cedc // indirect
)
diff --git a/go.sum b/go.sum
@@ -1,3 +1,7 @@
+github.com/aead/argon2 v0.0.0-20180111183520-a87724528b07 h1:i9/M2RadeVsPBMNwXFiaYkXQi9lY9VuZeI4Onavd3pA=
+github.com/aead/argon2 v0.0.0-20180111183520-a87724528b07/go.mod h1:Tnm/osX+XXr9R+S71o5/F0E60sRkPVALdhWw25qPImQ=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -12,7 +16,16 @@ github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/tobischo/gokeepasslib/v3 v3.4.1 h1:K7PwcVL4bUCmVFYQUNoBlUhl5GMPu67pY6QL07GL81Q=
+github.com/tobischo/gokeepasslib/v3 v3.4.1/go.mod h1:iwxOzUuk/ccA0mitrFC4MovT1p0IRY8EA35L4u1x/ug=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220913120320-3275c407cedc h1:dpclq5m2YrqPGStKmtw7IcNbKLfbIqKXvNxDJKdIKYc=
golang.org/x/sys v0.0.0-20220913120320-3275c407cedc/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/encrypt/core.go b/internal/encrypt/core.go
@@ -8,12 +8,9 @@ import (
"io"
random "math/rand"
"os"
- "os/exec"
- "strings"
"time"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/google/shlex"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
)
@@ -23,10 +20,6 @@ const (
nonceLength = 24
padLength = 256
saltLength = 16
- // PlainKeyMode is plaintext based key resolution.
- PlainKeyMode = "plaintext"
- // CommandKeyMode will run an external command to get the key (from stdout).
- CommandKeyMode = "command"
)
var (
@@ -73,29 +66,10 @@ func NewLockbox(options LockboxOptions) (Lockbox, error) {
}
func newLockbox(key, keyMode, file string) (Lockbox, error) {
- useKeyMode := keyMode
- if useKeyMode == "" {
- useKeyMode = os.Getenv(inputs.KeyModeEnv)
- }
- if useKeyMode == "" {
- useKeyMode = CommandKeyMode
- }
- useKey := key
- if useKey == "" {
- useKey = os.Getenv(inputs.KeyEnv)
- }
- if useKey == "" {
- return Lockbox{}, errors.New("no key given")
- }
- b, err := getKey(useKeyMode, useKey)
+ b, err := inputs.GetKey(key, keyMode)
if err != nil {
return Lockbox{}, err
}
-
- if len(b) == 0 {
- return Lockbox{}, errors.New("key is empty")
- }
-
var secretKey [keyLength]byte
copy(secretKey[:], b)
return Lockbox{secret: secretKey, file: file}, nil
@@ -111,28 +85,6 @@ func pad(salt, key []byte) ([keyLength]byte, error) {
return obj, nil
}
-func getKey(keyMode, name string) ([]byte, error) {
- var data []byte
- switch keyMode {
- case CommandKeyMode:
- parts, err := shlex.Split(name)
- if err != nil {
- return nil, err
- }
- cmd := exec.Command(parts[0], parts[1:]...)
- b, err := cmd.Output()
- if err != nil {
- return nil, err
- }
- data = b
- case PlainKeyMode:
- data = []byte(name)
- default:
- return nil, errors.New("unknown keymode")
- }
- return []byte(strings.TrimSpace(string(data))), nil
-}
-
func init() {
random.Seed(time.Now().UnixNano())
}
diff --git a/internal/encrypt/core_test.go b/internal/encrypt/core_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/enckse/lockbox/internal/encrypt"
+ "github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/store"
)
@@ -26,7 +27,7 @@ func setupData(t *testing.T) string {
}
func TestEncryptDecryptCommand(t *testing.T) {
- e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "echo test", KeyMode: encrypt.CommandKeyMode, File: setupData(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)
}
@@ -49,7 +50,7 @@ func TestEmptyKey(t *testing.T) {
if err == nil || err.Error() != "no key given" {
t.Errorf("invalid error: %v", err)
}
- _, err = encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: encrypt.CommandKeyMode, Key: "echo"})
+ _, err = encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: inputs.CommandKeyMode, Key: "echo"})
if err == nil || err.Error() != "key is empty" {
t.Errorf("invalid error: %v", err)
}
@@ -59,7 +60,7 @@ func TestKeyLength(t *testing.T) {
val := ""
for i := 0; i < 42; i++ {
val = fmt.Sprintf("a%s", val)
- _, err := encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: encrypt.PlainKeyMode, Key: val})
+ _, err := encrypt.NewLockbox(encrypt.LockboxOptions{KeyMode: inputs.PlainKeyMode, Key: val})
if err != nil {
t.Error("no error expected")
}
@@ -74,7 +75,7 @@ func TestUnknownMode(t *testing.T) {
}
func TestEncryptDecryptPlainText(t *testing.T) {
- e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "plain", KeyMode: encrypt.PlainKeyMode, File: setupData(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)
}
diff --git a/internal/inputs/env.go b/internal/inputs/env.go
@@ -2,9 +2,13 @@
package inputs
import (
+ "errors"
"fmt"
"os"
+ "os/exec"
"strings"
+
+ "github.com/google/shlex"
)
const (
@@ -30,6 +34,10 @@ const (
ClipMaxEnv = prefixKey + "CLIPMAX"
// ColorBetweenEnv is a comma-delimited list of times to color totp outputs (e.g. 0:5,30:35 which is the default).
ColorBetweenEnv = prefixKey + "TOTPBETWEEN"
+ // PlainKeyMode is plaintext based key resolution.
+ PlainKeyMode = "plaintext"
+ // CommandKeyMode will run an external command to get the key (from stdout).
+ CommandKeyMode = "command"
)
// EnvOrDefault will get the environment value OR default if env is not set.
@@ -41,6 +49,54 @@ func EnvOrDefault(envKey, defaultValue string) string {
return val
}
+// GetKey will get the encryption key setup for lb
+func GetKey(key, keyMode string) ([]byte, error) {
+ useKeyMode := keyMode
+ if useKeyMode == "" {
+ useKeyMode = os.Getenv(KeyModeEnv)
+ }
+ if useKeyMode == "" {
+ useKeyMode = CommandKeyMode
+ }
+ useKey := key
+ if useKey == "" {
+ useKey = os.Getenv(KeyEnv)
+ }
+ if useKey == "" {
+ return nil, errors.New("no key given")
+ }
+ b, err := getKey(useKeyMode, useKey)
+ if err != nil {
+ return nil, err
+ }
+ if len(b) == 0 {
+ return nil, errors.New("key is empty")
+ }
+ return b, nil
+}
+
+func getKey(keyMode, name string) ([]byte, error) {
+ var data []byte
+ switch keyMode {
+ case CommandKeyMode:
+ parts, err := shlex.Split(name)
+ if err != nil {
+ return nil, err
+ }
+ cmd := exec.Command(parts[0], parts[1:]...)
+ b, err := cmd.Output()
+ if err != nil {
+ return nil, err
+ }
+ data = b
+ case PlainKeyMode:
+ data = []byte(name)
+ default:
+ return nil, errors.New("unknown keymode")
+ }
+ return []byte(strings.TrimSpace(string(data))), nil
+}
+
func isYesNoEnv(defaultValue bool, env string) (bool, error) {
value := strings.ToLower(strings.TrimSpace(os.Getenv(env)))
if len(value) == 0 {
diff --git a/internal/subcommands/display.go b/internal/subcommands/display.go
@@ -21,6 +21,7 @@ type (
Entry string
Show bool
Glob string
+ All bool
Store store.FileSystem
}
)
@@ -28,8 +29,8 @@ type (
// DisplayCallback handles getting entries for display.
func DisplayCallback(args DisplayOptions) ([]dump.ExportEntity, error) {
entries := []string{args.Entry}
- if strings.Contains(args.Entry, "*") {
- if args.Entry == args.Glob {
+ 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
diff --git a/internal/subcommands/kdbx.go b/internal/subcommands/kdbx.go
@@ -0,0 +1,90 @@
+package subcommands
+
+import (
+ "errors"
+ "flag"
+ "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
+ }
+ root := gokeepasslib.NewGroup()
+ root.Name = "root"
+ 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)))
+ multi := len(strings.Split(strings.TrimSpace(val), "\n")) > 1
+ if multi {
+ e.Values = append(e.Values, value("Notes", val))
+ } else {
+ e.Values = append(e.Values, protectedValue("Password", val))
+ }
+ root.Entries = append(root.Entries, e)
+ }
+ db := &gokeepasslib.Database{
+ Header: gokeepasslib.NewHeader(),
+ Credentials: gokeepasslib.NewPasswordCredentials(key),
+ Content: &gokeepasslib.DBContent{
+ Meta: gokeepasslib.NewMetaData(),
+ 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
+ }
+ return nil
+}