lockbox

password manager
Log | Files | Refs | README | LICENSE

commit fde306fae016696a2298addffa1f07e4eeffb1fc
parent 203dcc2fcbca88f70d1b6bf725461fb7f78f1567
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Jun 2025 17:07:05 -0400

add help about fields, better field error checking

Diffstat:
Mcmd/lb/tests/expected.log | 6+++---
Minternal/app/help/core.go | 18++++++++++++++++++
Minternal/app/help/doc/database.txt | 7+++++++
Minternal/app/insert.go | 9++++++++-
Minternal/app/insert_test.go | 11++++++++++-
Minternal/app/totp.go | 4++--
Minternal/backend/actions.go | 6+++---
Minternal/backend/core.go | 18+++++++++---------
8 files changed, 60 insertions(+), 19 deletions(-)

diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -1,7 +1,7 @@ password: test1/key1/password -input paths must contain at LEAST 2 components -unknown entity field: -unknown entity field: still +'test3' is not an allowed field name +'' is not an allowed field name +'still' is not an allowed field name otp can NOT be multi-line password can NOT be multi-line testing5 diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -11,6 +11,7 @@ import ( "text/template" "git.sr.ht/~enckse/lockbox/internal/app/commands" + "git.sr.ht/~enckse/lockbox/internal/backend" "git.sr.ht/~enckse/lockbox/internal/config" "git.sr.ht/~enckse/lockbox/internal/output" ) @@ -43,6 +44,10 @@ type ( KeyFile string NoKey string } + Database struct { + Fields string + Examples string + } } ) @@ -112,6 +117,19 @@ func Usage(verbose bool, exe string) ([]string, error) { document.Config.XDG = config.ConfigXDG document.ReKey.KeyFile = setDocFlag(commands.ReKeyFlags.KeyFile) document.ReKey.NoKey = commands.ReKeyFlags.NoKey + var fields []string + for _, field := range backend.AllowedFields { + fields = append(fields, strings.ToLower(field)) + } + sort.Strings(fields) + document.Database.Fields = strings.Join(fields, ", ") + var examples []string + for _, example := range []string{commands.Insert, commands.Show} { + for _, field := range fields { + examples = append(examples, fmt.Sprintf("%s %s my/path/%s", document.Executable, example, field)) + } + } + document.Database.Examples = strings.Join(examples, "\n\n") files, err := docs.ReadDir(docDir) if err != nil { return nil, err diff --git a/internal/app/help/doc/database.txt b/internal/app/help/doc/database.txt @@ -6,3 +6,10 @@ is possible to use the database in those applications, just take caution when changing it outside of '{{ $.Executable }}' usage. If a database not normally used by '{{ $.Executable }}' is to be used by '{{ $.Executable }}', try using the various readonly settings to control interactions. + +When using `{{ $.Executable }}` one can only insert/manage the following +fields: {{ $.Database.Fields }} + +Example commands: + +{{ $.Database.Examples }} diff --git a/internal/app/insert.go b/internal/app/insert.go @@ -4,6 +4,7 @@ package app import ( "errors" "fmt" + "slices" "strings" "git.sr.ht/~enckse/lockbox/internal/backend" @@ -18,6 +19,12 @@ func Insert(cmd UserInputOptions) error { } entry := args[0] base := backend.Base(entry) + if !slices.ContainsFunc(backend.AllowedFields, func(v string) bool { + return base == strings.ToLower(v) + }) { + return fmt.Errorf("'%s' is not an allowed field name", base) + } + dir := backend.Directory(entry) existing, err := t.Get(dir, backend.SecretValue) if err != nil { @@ -33,7 +40,7 @@ func Insert(cmd UserInputOptions) error { } } } - password, err := cmd.Input(!isPipe && !strings.EqualFold(base, backend.Notes)) + password, err := cmd.Input(!isPipe && !strings.EqualFold(base, backend.NotesField)) if err != nil { return fmt.Errorf("invalid input: %w", err) } diff --git a/internal/app/insert_test.go b/internal/app/insert_test.go @@ -65,7 +65,7 @@ func TestInsertDo(t *testing.T) { m.pipe = func() bool { return false } - m.command.args = []string{"test/test2/test3"} + m.command.args = []string{"test/test2/test3ss/password"} m.command.confirm = false m.input = func() ([]byte, error) { return nil, errors.New("failure") @@ -75,12 +75,21 @@ func TestInsertDo(t *testing.T) { t.Errorf("invalid error: %v", err) } m.command.confirm = false + m.command.args = []string{"test/test2/test3/password"} m.pipe = func() bool { return true } if err := app.Insert(m); err == nil || err.Error() != "invalid input: failure" { t.Errorf("invalid error: %v", err) } + m.command.confirm = false + m.command.args = []string{"test/test2/test3/Password"} + m.pipe = func() bool { + return true + } + if err := app.Insert(m); err == nil || err.Error() != "'Password' is not an allowed field name" { + t.Errorf("invalid error: %v", err) + } m.input = func() ([]byte, error) { return []byte("TEST"), nil } diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -97,7 +97,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error { if !interactive && clipMode { return errors.New("clipboard not available in non-interactive mode") } - if !backend.IsLeafAttribute(args.Entry, backend.OTP) { + if !backend.IsLeafAttribute(args.Entry, backend.OTPField) { return fmt.Errorf("'%s' is not a TOTP entry", args.Entry) } entity, err := getEntity(args.Entry, opts.app) @@ -219,7 +219,7 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error { return ErrNoTOTP } if args.Mode == ListTOTPMode { - return doList(backend.OTP, args.Entry, opts.app, false) + return doList(backend.OTPField, args.Entry, opts.app, false) } return args.display(opts) } diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -185,7 +185,7 @@ func (t *Transaction) Move(src *Entity, dst string) error { values := make(map[string]string) for k, v := range src.Values { found := false - for _, mapping := range allowedFields { + for _, mapping := range AllowedFields { if strings.EqualFold(k, mapping) { values[mapping] = v found = true @@ -225,11 +225,11 @@ func (t *Transaction) Move(src *Entity, dst string) error { for k, v := range values { val := v switch k { - case otpKey, passKey: + case OTPField, PasswordField: if strings.Contains(val, "\n") { return fmt.Errorf("%s can NOT be multi-line", strings.ToLower(k)) } - if k == otpKey { + if k == OTPField { val = config.EnvTOTPFormat.Get(v) } } diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -15,22 +15,22 @@ import ( ) var ( - errPath = errors.New("input paths must contain at LEAST 2 components") - allowedFields = []string{notesKey, passKey, otpKey} + errPath = errors.New("input paths must contain at LEAST 2 components") + // AllowedFields are the same of allowed names for storing in a kdbx entry + AllowedFields = []string{NotesField, OTPField, PasswordField} ) const ( - notesKey = "Notes" titleKey = "Title" - passKey = "Password" pathSep = "/" isGlob = pathSep + "*" modTimeKey = "ModTime" - otpKey = "otp" - // OTP is the totp storage attribute - OTP = otpKey - // Notes is the multiline notes key - Notes = notesKey + // OTPField is the totp storage attribute + OTPField = "otp" + // NotesField is the multiline notes key + NotesField = "Notes" + // PasswordField is where the password is stored + PasswordField = "Password" ) type (