lockbox

password manager
Log | Files | Refs | README | LICENSE

commit f6727077df18eb469ff83e3d78a43b6ad37a702f
parent 4bab0442b6cf611a0b1f11921604fc953a1baa47
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  1 Oct 2022 12:41:48 -0400

splitting up/renaming items

Diffstat:
Ainternal/backend/actions.go | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/backend/core.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/backend/query.go | 42------------------------------------------
Dinternal/backend/transact.go | 215-------------------------------------------------------------------------------
Ainternal/backend/types.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 267 insertions(+), 257 deletions(-)

diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -0,0 +1,137 @@ +// Package backend handles kdbx interactions +package backend + +import ( + "errors" + "os" + "path/filepath" + + "github.com/enckse/lockbox/internal/inputs" + "github.com/tobischo/gokeepasslib/v3" + "github.com/tobischo/gokeepasslib/v3/wrappers" +) + +func (t *Transaction) act(cb action) error { + if !t.valid { + return errors.New("invalid transaction") + } + key, err := inputs.GetKey("", "") + if err != nil { + return err + } + k := string(key) + if !t.exists { + if err := create(t.file, k); err != nil { + return err + } + } + f, err := os.Open(t.file) + if err != nil { + return err + } + defer f.Close() + db := gokeepasslib.NewDatabase() + db.Credentials = gokeepasslib.NewPasswordCredentials(k) + if err := gokeepasslib.NewDecoder(f).Decode(db); err != nil { + return err + } + if len(db.Content.Root.Groups) != 1 { + return errors.New("kdbx must only have ONE root group") + } + cErr := cb(Context{db: db}) + if t.write { + if err := db.LockProtectedEntries(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + f, err = os.Create(t.file) + if err != nil { + return err + } + defer f.Close() + return encode(f, db) + } + return cErr +} + +func (t *Transaction) change(cb action) error { + return t.act(func(c Context) error { + if err := c.db.UnlockProtectedEntries(); err != nil { + return err + } + t.write = true + return cb(c) + }) +} + +// Insert handles inserting a new element +func (t *Transaction) Insert(path, val string, entity *QueryEntity, multi bool) error { + return t.change(func(c Context) error { + if entity != nil { + if err := remove(entity, c); err != nil { + return err + } + } + e := gokeepasslib.NewEntry() + e.Values = append(e.Values, value(titleKey, filepath.Dir(path))) + e.Values = append(e.Values, value(userNameKey, filepath.Base(path))) + field := passKey + if multi { + field = notesKey + } + + e.Values = append(e.Values, protectedValue(field, val)) + c.db.Content.Root.Groups[0].Entries = append(c.db.Content.Root.Groups[0].Entries, e) + return nil + }) +} + +func remove(entity *QueryEntity, c Context) error { + entries := c.db.Content.Root.Groups[0].Entries + idx := -1 + for i, e := range entries { + if entity.Path == getPathName(e) { + idx = i + } + } + if idx < 0 { + return errors.New("unable to select entity for deletion") + } + c.db.Content.Root.Groups[0].Entries = append(entries[:idx], entries[idx+1:]...) + return nil +} + +// Remove handles remove an element +func (t *Transaction) Remove(entity *QueryEntity) error { + if entity == nil { + return errors.New("entity is empty/invalid") + } + return t.change(func(c Context) error { + return remove(entity, c) + }) +} + +func getValue(e gokeepasslib.Entry, key string) string { + v := e.Get(key) + if v == nil { + return "" + } + return v.Value.Content +} + +func value(key string, value string) gokeepasslib.ValueData { + return gokeepasslib.ValueData{Key: key, Value: gokeepasslib.V{Content: value}} +} + +func getPathName(entry gokeepasslib.Entry) string { + return filepath.Join(entry.GetTitle(), getValue(entry, userNameKey)) +} + +func protectedValue(key string, value string) gokeepasslib.ValueData { + return gokeepasslib.ValueData{ + Key: key, + Value: gokeepasslib.V{Content: value, Protected: wrappers.NewBoolWrapper(true)}, + } +} diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -0,0 +1,65 @@ +// Package backend handles kdbx interactions +package backend + +import ( + "errors" + "os" + "strings" + + "github.com/enckse/lockbox/internal/inputs" + "github.com/tobischo/gokeepasslib/v3" +) + +// Load will load a kdbx file for transactions +func Load(file string) (*Transaction, error) { + return loadFile(file, true) +} + +func loadFile(file string, must bool) (*Transaction, error) { + if !strings.HasSuffix(file, ".kdbx") { + return nil, errors.New("should use a .kdbx extension") + } + exists := pathExists(file) + if must { + if !exists { + return nil, errors.New("invalid file, does not exists") + } + } + return &Transaction{valid: true, file: file, exists: exists}, nil +} + +// NewTransaction will use the underlying environment data store location +func NewTransaction() (*Transaction, error) { + return loadFile(os.Getenv(inputs.StoreEnv), false) +} + +func create(file, key string) error { + root := gokeepasslib.NewGroup() + root.Name = "root" + db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4()) + db.Credentials = gokeepasslib.NewPasswordCredentials(key) + db.Content.Root = + &gokeepasslib.RootData{ + Groups: []gokeepasslib.Group{root}, + } + f, err := os.Create(file) + if err != nil { + return err + } + defer f.Close() + return encode(f, db) +} + +func encode(f *os.File, db *gokeepasslib.Database) error { + return gokeepasslib.NewEncoder(f).Encode(db) +} + +// 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 +} diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -9,48 +9,6 @@ import ( "path/filepath" "sort" "strings" - - "github.com/tobischo/gokeepasslib/v3" -) - -type ( - // QueryMode indicates HOW an entity will be found - QueryMode int - // ValueMode indicates what to do with the store value of the entity - ValueMode int - // QueryOptions indicates how to find entities - QueryOptions struct { - Mode QueryMode - Values ValueMode - Criteria string - } - // QueryEntity is the result of a query - QueryEntity struct { - Path string - Value string - backing gokeepasslib.Entry - } -) - -const ( - noneMode QueryMode = iota - // ListMode indicates ALL entities will be listed - ListMode - // FindMode indicates a _contains_ search for an entity - FindMode - // ExactMode means an entity must MATCH the string exactly - ExactMode - // SuffixMode will look for an entity ending in a specific value - SuffixMode -) - -const ( - // BlankValue will not decrypt secrets, empty value - BlankValue ValueMode = iota - // HashedValue will decrypt and then hash the password - HashedValue - // SecretValue will have the raw secret onboard - SecretValue ) // Get will request a singular entity diff --git a/internal/backend/transact.go b/internal/backend/transact.go @@ -1,215 +0,0 @@ -// Package backend handles kdbx interactions -package backend - -import ( - "errors" - "os" - "path/filepath" - "strings" - - "github.com/enckse/lockbox/internal/inputs" - "github.com/tobischo/gokeepasslib/v3" - "github.com/tobischo/gokeepasslib/v3/wrappers" -) - -const ( - userNameKey = "UserName" - notesKey = "Notes" - titleKey = "Title" - passKey = "Password" -) - -type ( - // action are transcation operations that more or less CRUD the kdbx file - action func(t Context) error - // Transaction handles the overall operation of the transaction - Transaction struct { - valid bool - file string - exists bool - write bool - } - // Context handles operating on the underlying database - Context struct { - db *gokeepasslib.Database - } -) - -// Load will load a kdbx file for transactions -func Load(file string) (*Transaction, error) { - return loadFile(file, true) -} - -func loadFile(file string, must bool) (*Transaction, error) { - if !strings.HasSuffix(file, ".kdbx") { - return nil, errors.New("should use a .kdbx extension") - } - exists := pathExists(file) - if must { - if !exists { - return nil, errors.New("invalid file, does not exists") - } - } - return &Transaction{valid: true, file: file, exists: exists}, nil -} - -// NewTransaction will use the underlying environment data store location -func NewTransaction() (*Transaction, error) { - return loadFile(os.Getenv(inputs.StoreEnv), false) -} - -func create(file, key string) error { - root := gokeepasslib.NewGroup() - root.Name = "root" - db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4()) - db.Credentials = gokeepasslib.NewPasswordCredentials(key) - db.Content.Root = - &gokeepasslib.RootData{ - Groups: []gokeepasslib.Group{root}, - } - f, err := os.Create(file) - if err != nil { - return err - } - defer f.Close() - return encode(f, db) -} - -func encode(f *os.File, db *gokeepasslib.Database) error { - return gokeepasslib.NewEncoder(f).Encode(db) -} - -func (t *Transaction) act(cb action) error { - if !t.valid { - return errors.New("invalid transaction") - } - key, err := inputs.GetKey("", "") - if err != nil { - return err - } - k := string(key) - if !t.exists { - if err := create(t.file, k); err != nil { - return err - } - } - f, err := os.Open(t.file) - if err != nil { - return err - } - defer f.Close() - db := gokeepasslib.NewDatabase() - db.Credentials = gokeepasslib.NewPasswordCredentials(k) - if err := gokeepasslib.NewDecoder(f).Decode(db); err != nil { - return err - } - if len(db.Content.Root.Groups) != 1 { - return errors.New("kdbx must only have ONE root group") - } - cErr := cb(Context{db: db}) - if t.write { - if err := db.LockProtectedEntries(); err != nil { - return err - } - if err := f.Close(); err != nil { - return err - } - f, err = os.Create(t.file) - if err != nil { - return err - } - defer f.Close() - return encode(f, db) - } - return cErr -} - -func (t *Transaction) change(cb action) error { - return t.act(func(c Context) error { - if err := c.db.UnlockProtectedEntries(); err != nil { - return err - } - t.write = true - return cb(c) - }) -} - -// Insert handles inserting a new element -func (t *Transaction) Insert(path, val string, entity *QueryEntity, multi bool) error { - return t.change(func(c Context) error { - if entity != nil { - if err := remove(entity, c); err != nil { - return err - } - } - e := gokeepasslib.NewEntry() - e.Values = append(e.Values, value(titleKey, filepath.Dir(path))) - e.Values = append(e.Values, value(userNameKey, filepath.Base(path))) - field := passKey - if multi { - field = notesKey - } - - e.Values = append(e.Values, protectedValue(field, val)) - c.db.Content.Root.Groups[0].Entries = append(c.db.Content.Root.Groups[0].Entries, e) - return nil - }) -} - -func remove(entity *QueryEntity, c Context) error { - entries := c.db.Content.Root.Groups[0].Entries - idx := -1 - for i, e := range entries { - if entity.Path == getPathName(e) { - idx = i - } - } - if idx < 0 { - return errors.New("unable to select entity for deletion") - } - c.db.Content.Root.Groups[0].Entries = append(entries[:idx], entries[idx+1:]...) - return nil -} - -// Remove handles remove an element -func (t *Transaction) Remove(entity *QueryEntity) error { - if entity == nil { - return errors.New("entity is empty/invalid") - } - return t.change(func(c Context) error { - return remove(entity, c) - }) -} - -func getValue(e gokeepasslib.Entry, key string) string { - v := e.Get(key) - if v == nil { - return "" - } - return v.Value.Content -} - -func value(key string, value string) gokeepasslib.ValueData { - return gokeepasslib.ValueData{Key: key, Value: gokeepasslib.V{Content: value}} -} - -func getPathName(entry gokeepasslib.Entry) string { - return filepath.Join(entry.GetTitle(), getValue(entry, userNameKey)) -} - -func protectedValue(key string, value string) gokeepasslib.ValueData { - return gokeepasslib.ValueData{ - Key: key, - Value: gokeepasslib.V{Content: value, Protected: wrappers.NewBoolWrapper(true)}, - } -} - -// 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 -} diff --git a/internal/backend/types.go b/internal/backend/types.go @@ -0,0 +1,65 @@ +// Package backend has types +package backend + +import ( + "github.com/tobischo/gokeepasslib/v3" +) + +type ( + // QueryMode indicates HOW an entity will be found + QueryMode int + // ValueMode indicates what to do with the store value of the entity + ValueMode int + // QueryOptions indicates how to find entities + QueryOptions struct { + Mode QueryMode + Values ValueMode + Criteria string + } + // QueryEntity is the result of a query + QueryEntity struct { + Path string + Value string + backing gokeepasslib.Entry + } + action func(t Context) error + // Transaction handles the overall operation of the transaction + Transaction struct { + valid bool + file string + exists bool + write bool + } + // Context handles operating on the underlying database + Context struct { + db *gokeepasslib.Database + } +) + +const ( + noneMode QueryMode = iota + // ListMode indicates ALL entities will be listed + ListMode + // FindMode indicates a _contains_ search for an entity + FindMode + // ExactMode means an entity must MATCH the string exactly + ExactMode + // SuffixMode will look for an entity ending in a specific value + SuffixMode +) + +const ( + // BlankValue will not decrypt secrets, empty value + BlankValue ValueMode = iota + // HashedValue will decrypt and then hash the password + HashedValue + // SecretValue will have the raw secret onboard + SecretValue +) + +const ( + userNameKey = "UserName" + notesKey = "Notes" + titleKey = "Title" + passKey = "Password" +)