commit 203dcc2fcbca88f70d1b6bf725461fb7f78f1567
parent 259309faacaa0b5b9924c31a8365e43729e84e29
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 7 Jun 2025 16:38:19 -0400
ultimately pwgen is out of scope
Diffstat:
13 files changed, 20 insertions(+), 300 deletions(-)
diff --git a/README.md b/README.md
@@ -13,7 +13,7 @@ keyring or command for password input over using a GPG key and uses a keepass da
While `lb` uses a `.kdbx` formatted file that can be opened by a variety of tools, it is highly opinionated on how to store data in the database. Any
`.kdbx` used with `lb` should be managed by `lb` with a fallback ability to use other tools to alter the/view the file otherwise. Mainly lockbox itself
is using a common format so that it doesn't lock a user into a custom file format or dealing with gpg, age, etc. files and instead COULD be recovered
-via other tooling if needed.
+via other tooling if needed. `lb` does try to place nice with standard fields used within kdbx files, but it may disagree how to manage/store/update them.
## configuration
@@ -54,10 +54,7 @@ lb clip my/secret/password
Create a new entry
```
-lb insert my/new/key
-# or
-lb multiline my/new/multi
-# for multiline inserts
+lb insert my/new/key/password
```
### list
@@ -78,19 +75,19 @@ lb rm my/old/key
To see the text of an entry
```
-lb show my/key/value
+lb show my/key/notes
```
### totp
To get a totp token
```
-lb totp show token
+lb totp show token/path/otp
```
The token can be automatically copied to the clipboard too
```
-lb totp clip token
+lb totp clip token/path/otp
```
### rekey
diff --git a/cmd/lb/main.go b/cmd/lb/main.go
@@ -104,8 +104,6 @@ func run() error {
return err
}
return args.Do(app.NewDefaultTOTPOptions(p))
- case commands.PasswordGenerate:
- return app.GeneratePassword(p)
default:
return fmt.Errorf("unknown command: %s", command)
}
diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go
@@ -50,8 +50,6 @@ const (
JSON = "json"
// CompletionsZsh is the command to generate zsh completions
CompletionsZsh = "zsh"
- // PasswordGenerate is the command to do password generation
- PasswordGenerate = "pwgen"
// Executable is the name of the executable
Executable = "lb"
// Unset indicates a value should be unset (removed) from an entity
diff --git a/internal/app/completions/core.go b/internal/app/completions/core.go
@@ -42,12 +42,11 @@ type (
// Conditionals help control completion flow
Conditionals struct {
Not struct {
- ReadOnly string
- CanClip string
- CanTOTP string
- AskMode string
- Ever string
- CanPasswordGen string
+ ReadOnly string
+ CanClip string
+ CanTOTP string
+ AskMode string
+ Ever string
}
Exported []string
}
@@ -91,7 +90,6 @@ func NewConditionals() Conditionals {
c.Not.CanClip = registerIsNotEqual(config.EnvClipEnabled, config.NoValue)
c.Not.CanTOTP = registerIsNotEqual(config.EnvTOTPEnabled, config.NoValue)
c.Not.AskMode = registerIsNotEqual(config.EnvPasswordMode, string(config.AskKeyMode))
- c.Not.CanPasswordGen = registerIsNotEqual(config.EnvPasswordGenEnabled, config.NoValue)
c.Not.Ever = fmt.Sprintf(shellIsNotText, "1", "0")
return c
}
@@ -124,13 +122,12 @@ func Generate(completionType, exe string) ([]string, error) {
c.Options = c.newGenOptions([]string{commands.Help, commands.List, commands.Show, commands.Version, commands.JSON, commands.Groups},
map[string]string{
- commands.Clip: c.Conditionals.Not.CanClip,
- commands.TOTP: c.Conditionals.Not.CanTOTP,
- commands.Move: c.Conditionals.Not.ReadOnly,
- commands.Remove: c.Conditionals.Not.ReadOnly,
- commands.Insert: c.Conditionals.Not.ReadOnly,
- commands.Unset: c.Conditionals.Not.ReadOnly,
- commands.PasswordGenerate: c.Conditionals.Not.CanPasswordGen,
+ commands.Clip: c.Conditionals.Not.CanClip,
+ commands.TOTP: c.Conditionals.Not.CanTOTP,
+ commands.Move: c.Conditionals.Not.ReadOnly,
+ commands.Remove: c.Conditionals.Not.ReadOnly,
+ commands.Insert: c.Conditionals.Not.ReadOnly,
+ commands.Unset: c.Conditionals.Not.ReadOnly,
})
c.TOTPSubCommands = c.newGenOptions([]string{commands.TOTPMinimal, commands.TOTPOnce, commands.TOTPShow},
map[string]string{
diff --git a/internal/app/completions/core_test.go b/internal/app/completions/core_test.go
@@ -23,7 +23,7 @@ func TestCompletions(t *testing.T) {
func TestConditionals(t *testing.T) {
c := completions.NewConditionals()
sort.Strings(c.Exported)
- need := []string{"LOCKBOX_CLIP_ENABLED", "LOCKBOX_CREDENTIALS_PASSWORD_MODE", "LOCKBOX_PWGEN_ENABLED", "LOCKBOX_READONLY", "LOCKBOX_TOTP_ENABLED"}
+ need := []string{"LOCKBOX_CLIP_ENABLED", "LOCKBOX_CREDENTIALS_PASSWORD_MODE", "LOCKBOX_READONLY", "LOCKBOX_TOTP_ENABLED"}
if len(c.Exported) != len(need) || fmt.Sprintf("%v", c.Exported) != fmt.Sprintf("%v", need) {
t.Errorf("invalid exports: %v", c.Exported)
}
diff --git a/internal/app/help/core.go b/internal/app/help/core.go
@@ -84,7 +84,6 @@ func Usage(verbose bool, exe string) ([]string, error) {
results = append(results, command(commands.List, isFilter, "list entries"))
results = append(results, command(commands.Groups, isFilter, "list groups"))
results = append(results, command(commands.Move, "src dst", "move a group from source to destination"))
- results = append(results, command(commands.PasswordGenerate, "", "generate a password"))
results = append(results, command(commands.ReKey, "", "rekey/reinitialize the database credentials"))
results = append(results, command(commands.Remove, "group", "remove an entry from the store"))
results = append(results, command(commands.Show, isEntry, "show the entry's value"))
diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go
@@ -9,11 +9,11 @@ import (
func TestUsage(t *testing.T) {
u, _ := help.Usage(false, "lb")
- if len(u) != 26 {
+ if len(u) != 25 {
t.Errorf("invalid usage, out of date? %d", len(u))
}
u, _ = help.Usage(true, "lb")
- if len(u) != 90 {
+ if len(u) != 89 {
t.Errorf("invalid verbose usage, out of date? %d", len(u))
}
for _, usage := range u {
diff --git a/internal/app/pwgen.go b/internal/app/pwgen.go
@@ -1,99 +0,0 @@
-// Package app can generate passwords
-package app
-
-import (
- "bytes"
- "errors"
- "fmt"
- "math/rand"
- "os/exec"
- "slices"
- "strings"
- "text/template"
-
- "git.sr.ht/~enckse/lockbox/internal/config"
-)
-
-// GeneratePassword generates a password
-func GeneratePassword(cmd CommandOptions) error {
- enabled := config.EnvPasswordGenEnabled.Get()
- if !enabled {
- return errors.New("password generation is disabled")
- }
- length, err := config.EnvPasswordGenWordCount.Get()
- if err != nil {
- return err
- }
- tmplString := config.EnvPasswordGenTemplate.Get()
- wordList := config.EnvPasswordGenWordList.Get()
- if len(wordList) == 0 {
- return errors.New("word list command must set")
- }
- exe := wordList[0]
- var args []string
- if len(wordList) > 1 {
- args = wordList[1:]
- }
- wordResults, err := exec.Command(exe, args...).Output()
- if err != nil {
- return err
- }
- chars := config.EnvPasswordGenChars.Get()
- hasChars := len(chars) > 0
- var allowedChars []rune
- if hasChars {
- for _, c := range chars {
- allowedChars = append(allowedChars, c)
- }
- }
- var choices []string
- for _, line := range strings.Split(string(wordResults), "\n") {
- t := strings.TrimSpace(line)
- if t == "" {
- continue
- }
- use := line
- if hasChars {
- res := ""
- for _, c := range use {
- if slices.Contains(allowedChars, c) {
- res = fmt.Sprintf("%s%c", res, c)
- }
- }
- if res == "" {
- continue
- }
- use = res
- }
- choices = append(choices, use)
- }
- found := len(choices)
- if found == 0 {
- return errors.New("no sources given")
- }
- var selected []config.Word
- var cnt int64
- totalLength := 0
- for cnt < length {
- choice := choices[rand.Intn(found)]
- textLength := len(choice)
- selected = append(selected, config.Word{Text: choice, Position: config.Position{Start: totalLength, End: totalLength + textLength}})
- totalLength += textLength
- cnt++
- }
- tmpl, err := template.New("t").Parse(tmplString)
- if err != nil {
- return err
- }
- var buf bytes.Buffer
- if err := tmpl.Execute(&buf, selected); err != nil {
- return err
- }
- if _, err := buf.WriteString("\n"); err != nil {
- return err
- }
- if _, err := cmd.Writer().Write(buf.Bytes()); err != nil {
- return err
- }
- return nil
-}
diff --git a/internal/app/pwgen_test.go b/internal/app/pwgen_test.go
@@ -1,106 +0,0 @@
-package app_test
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "git.sr.ht/~enckse/lockbox/internal/app"
- "git.sr.ht/~enckse/lockbox/internal/config/store"
-)
-
-func setupGenScript() string {
- store.Clear()
- os.Clearenv()
- const pwgenScript = "pwgen.sh"
- pwgenPath := filepath.Join("testdata", pwgenScript)
- os.WriteFile(pwgenPath, []byte(`#!/bin/sh
-for f in $@; do
- echo $f
-done
-`), 0o755)
- return pwgenPath
-}
-
-func TestGenerateError(t *testing.T) {
- m := newMockCommand(t)
- pwgenPath := setupGenScript()
- store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 0)
- if err := app.GeneratePassword(m); err == nil || err.Error() != "word count must be > 0" {
- t.Errorf("invalid error: %v", err)
- }
- store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 1)
- if err := app.GeneratePassword(m); err == nil || err.Error() != "word list command must set" {
- t.Errorf("invalid error: %v", err)
- }
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{"1 x"})
- if err := app.GeneratePassword(m); err == nil || !strings.Contains(err.Error(), "exec: \"1 x\":") {
- t.Errorf("invalid error: %v", err)
- }
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath})
- if err := app.GeneratePassword(m); err == nil || err.Error() != "no sources given" {
- t.Errorf("invalid error: %v", err)
- }
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "1"})
- if err := app.GeneratePassword(m); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "aloj", "1"})
- if err := app.GeneratePassword(m); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- store.SetBool("LOCKBOX_PWGEN_ENABLED", false)
- if err := app.GeneratePassword(m); err == nil || err.Error() != "password generation is disabled" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func testPasswordGen(t *testing.T, expect string) {
- m := newMockCommand(t)
- if err := app.GeneratePassword(m); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- s := m.buf.String()
- if s != fmt.Sprintf("%s\n", expect) {
- t.Errorf("invalid generated: %s (expected: %s)", s, expect)
- }
-}
-
-func TestGenerate(t *testing.T) {
- pwgenPath := setupGenScript()
- store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 1)
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "1"})
- testPasswordGen(t, "1")
- store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 10)
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "1 1 1 1 1 1 1 1 1 1 1 1"})
- testPasswordGen(t, "1-1-1-1-1-1-1-1-1-1")
- store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 4)
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "a a a a a a a a a a a a a a a a a a a a a a"})
- testPasswordGen(t, "a-a-a-a")
- store.SetString("LOCKBOX_PWGEN_CHARACTERS", "bc")
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "abc abc abc abc abc abc aaa aa aaa a"})
- testPasswordGen(t, "bc-bc-bc-bc")
- store.SetString("LOCKBOX_PWGEN_CHARACTERS", "")
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "a a a a a a a a a a a a a a a a a a a a a a"})
- testPasswordGen(t, "a-a-a-a")
- // NOTE: this allows templating below in golang
- store.SetString("LOCKBOX_PWGEN_TEMPLATE", "{{range $idx, $val := .}}{{if lt $idx 5}}-{{end}}{{ $val.Text }}{{ $val.Position.Start }}{{ $val.Position.End }}{{end}}")
- testPasswordGen(t, "-a01-a12-a23-a34")
- store.SetString("LOCKBOX_PWGEN_TEMPLATE", "{{range $idx, $val := .}}{{if lt $idx 5}}-{{end}}{{ $val.Text }}{{end}}")
- testPasswordGen(t, "-a-a-a-a")
- store.Clear()
- store.SetArray("LOCKBOX_PWGEN_WORD_COMMAND", []string{pwgenPath, "abc axy axY aZZZ aoijafea aoiajfoea afeafa"})
- m := newMockCommand(t)
- if err := app.GeneratePassword(m); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- s := m.buf.String()
- if s[0] != 'a' {
- t.Errorf("no title: %s", s)
- }
- if len(s) < 5 {
- t.Errorf("bad result: %s", s)
- }
-}
diff --git a/internal/config/core.go b/internal/config/core.go
@@ -18,7 +18,6 @@ const (
// sub categories
clipCategory = "CLIP_"
totpCategory = "TOTP_"
- genCategory = "PWGEN_"
jsonCategory = "JSON_"
credsCategory = "CREDENTIALS_"
defaultCategory = "DEFAULTS_"
diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go
@@ -251,7 +251,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) {
if err := config.LoadConfigFile(file); err != nil {
t.Errorf("invalid error: %v", err)
}
- if len(store.List()) != 25 {
+ if len(store.List()) != 20 {
t.Errorf("invalid environment after load")
}
}
diff --git a/internal/config/vars.go b/internal/config/vars.go
@@ -217,56 +217,4 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode),
flags: []stringsFlags{canExpandFlag},
},
})
- // EnvPasswordGenWordCount is the number of words that will be selected for password generation
- EnvPasswordGenWordCount = environmentRegister(EnvironmentInt{
- environmentDefault: newDefaultedEnvironment(8,
- environmentBase{
- key: genCategory + "WORD_COUNT",
- description: "Number of words to select and include in the generated password.",
- }),
- short: "word count",
- })
- // EnvPasswordGenTemplate is the output template for controlling how output words are placed together
- EnvPasswordGenTemplate = environmentRegister(EnvironmentString{
- environmentStrings: environmentStrings{
- environmentDefault: newDefaultedEnvironment("{{range $i, $val := .}}{{if $i}}-{{end}}{{$val.Text}}{{end}}",
- environmentBase{
- key: genCategory + "TEMPLATE",
- description: fmt.Sprintf("The go text template to use to format the chosen words into a password. Available fields: %s.", TextPositionFields()),
- }),
- allowed: []string{"<go template>"},
- flags: []stringsFlags{canDefaultFlag},
- },
- })
- // EnvPasswordGenWordList is the command text to generate the word list
- EnvPasswordGenWordList = environmentRegister(EnvironmentArray{
- environmentStrings: environmentStrings{
- environmentDefault: newDefaultedEnvironment("",
- environmentBase{
- key: genCategory + "WORD_COMMAND",
- description: "Command to retrieve the word list to use for password generation (must be split by newline).",
- }),
- flags: []stringsFlags{isCommandFlag},
- },
- })
- // EnvPasswordGenEnabled indicates if password generation is enabled
- EnvPasswordGenEnabled = environmentRegister(EnvironmentBool{
- environmentDefault: newDefaultedEnvironment(true,
- environmentBase{
- key: genCategory + "ENABLED",
- description: "Enable password generation.",
- }),
- })
- // EnvPasswordGenChars allows for restricting which characters can be used
- EnvPasswordGenChars = environmentRegister(EnvironmentString{
- environmentStrings: environmentStrings{
- environmentDefault: newDefaultedEnvironment("",
- environmentBase{
- key: genCategory + "CHARACTERS",
- description: "The set of allowed characters in output words (empty means any character is allowed).",
- }),
- allowed: []string{"<list of characters>"},
- flags: []stringsFlags{canDefaultFlag},
- },
- })
)
diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go
@@ -48,10 +48,6 @@ func TestIsNoClip(t *testing.T) {
checkYesNo("LOCKBOX_CLIP_ENABLED", t, config.EnvClipEnabled, true)
}
-func TestIsNoGeneratePassword(t *testing.T) {
- checkYesNo("LOCKBOX_PWGEN_ENABLED", t, config.EnvPasswordGenEnabled, true)
-}
-
func TestFormatTOTP(t *testing.T) {
store.Clear()
otp := config.EnvTOTPFormat.Get("otpauth://abc")
@@ -85,10 +81,6 @@ func TestMaxTOTP(t *testing.T) {
checkInt(config.EnvTOTPTimeout, "LOCKBOX_TOTP_TIMEOUT", "max totp time", 120, false, t)
}
-func TestWordCount(t *testing.T) {
- checkInt(config.EnvPasswordGenWordCount, "LOCKBOX_PWGEN_WORD_COUNT", "word count", 8, false, t)
-}
-
func checkInt(e config.EnvironmentInt, key, text string, def int64, allowZero bool, t *testing.T) {
store.Clear()
val, err := e.Get()
@@ -139,7 +131,6 @@ func TestUnsetArrays(t *testing.T) {
for _, i := range []config.EnvironmentArray{
config.EnvClipCopy,
config.EnvClipPaste,
- config.EnvPasswordGenWordList,
} {
val := i.Get()
if len(val) != 0 {
@@ -158,7 +149,6 @@ func TestDefaultStrings(t *testing.T) {
for k, v := range map[string]config.EnvironmentString{
"hash": config.EnvJSONMode,
"command": config.EnvPasswordMode,
- "{{range $i, $val := .}}{{if $i}}-{{end}}{{$val.Text}}{{end}}": config.EnvPasswordGenTemplate,
} {
val := v.Get()
if val != k {
@@ -179,7 +169,6 @@ func TestEmptyStrings(t *testing.T) {
config.EnvStore,
config.EnvKeyFile,
config.EnvDefaultModTime,
- config.EnvPasswordGenChars,
} {
val := v.Get()
if val != "" {