diff --git a/README.md b/README.md index 2913576..1f81c2e 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Flags | `-json` | Output as JSON to stdout | | `-pin` | Enable Quick Unlock using a PIN | | `-and` | Combines filters with AND instead of default OR | +| `-exact` | Matches filters against the entire title/subtitle (case-insensitive) instead of as substrings | | `-sort` | Sort the output by title and username of the `list` and `show` command | | `-trashed` | Show trashed items in the `list` and `show` command | | `-detailed` | Show every field of each entry in `list` and `show` instead of only the summary fields (title, login, category, label, type) | diff --git a/cmd/enpasscli/main.go b/cmd/enpasscli/main.go index 366ac9f..f68dd12 100644 --- a/cmd/enpasscli/main.go +++ b/cmd/enpasscli/main.go @@ -72,6 +72,7 @@ type Args struct { trashed *bool detailed *bool and *bool + exact *bool clipboardPrimary *bool field *string // write command flags @@ -93,6 +94,7 @@ func (args *Args) parse() { args.nonInteractive = flag.Bool("nonInteractive", false, "Disable prompts and fail instead.") args.pinEnable = flag.Bool("pin", false, "Enable PIN.") args.and = flag.Bool("and", false, "Combines filters with AND instead of default OR.") + args.exact = flag.Bool("exact", false, "Matches filters against the entire title/subtitle (case-insensitive) instead of as substrings.") args.sort = flag.Bool("sort", false, "Sort the output by title and username of the 'list' and 'show' command.") args.trashed = flag.Bool("trashed", false, "Show trashed items in the 'list' and 'show' command.") args.detailed = flag.Bool("detailed", false, "Show every field of each entry in 'list' and 'show'. Without this flag, only the original summary fields (title, login, category, label, type) are displayed.") @@ -833,6 +835,7 @@ func trashEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) { func restoreEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) { // For restore, we need to look in trashed items vault.FilterAnd = *args.and + vault.FilterExact = *args.exact cards, err := vault.GetEntries(*args.cardType, args.filters) if err != nil { logger.WithError(err).Fatal("could not retrieve entries") @@ -870,6 +873,7 @@ func restoreEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) { func deleteEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) { // For delete, we need to look in trashed items vault.FilterAnd = *args.and + vault.FilterExact = *args.exact cards, err := vault.GetEntries(*args.cardType, args.filters) if err != nil { logger.WithError(err).Fatal("could not retrieve entries") @@ -975,6 +979,7 @@ func main() { logger.WithError(err).Fatal("could not create vault") } vault.FilterAnd = *args.and + vault.FilterExact = *args.exact var store *unlock.SecureStore if !*args.pinEnable { diff --git a/pkg/enpass/vault.go b/pkg/enpass/vault.go index 95bf488..5a8a9e7 100644 --- a/pkg/enpass/vault.go +++ b/pkg/enpass/vault.go @@ -29,6 +29,7 @@ type Vault struct { // settings for filtering entries FilterFields []string FilterAnd bool + FilterExact bool // vault.enpassdb : SQLCipher database databaseFilename string @@ -352,7 +353,11 @@ func (v *Vault) executeEntryQuery(cardType string, filters []string) (*sql.Rows, for _, filter := range filters { fq := "(0" for _, field := range v.FilterFields { - fq += " + instr(lower(" + field + "), ?)" + if v.FilterExact { + fq += " + (lower(" + field + ") = ?)" + } else { + fq += " + instr(lower(" + field + "), ?)" + } values = append(values, strings.ToLower(filter)) } fq += " > 0)" diff --git a/pkg/enpass/vault_test.go b/pkg/enpass/vault_test.go index 142a738..5fd9523 100644 --- a/pkg/enpass/vault_test.go +++ b/pkg/enpass/vault_test.go @@ -79,6 +79,27 @@ func TestVault_GetEntries_Filter_AND(t *testing.T) { Assert_GetEntries(t, vault, []string{"inexistent", "alsoinexistent"}, 0) } +func TestVault_GetEntries_Filter_Exact(t *testing.T) { + vault, err := NewVault(vaultPath, logrus.ErrorLevel) + if err != nil { + t.Errorf("vault initialization failed: %+v", err) + } + defer vault.Close() + credentials := &VaultCredentials{Password: testPassword} + if err := vault.Open(credentials); err != nil { + t.Errorf("opening vault failed: %+v", err) + } + + vault.FilterExact = true + + Assert_GetEntries(t, vault, []string{"Whatever"}, 1) // matches full title + Assert_GetEntries(t, vault, []string{"whatever"}, 1) // case-insensitive + Assert_GetEntries(t, vault, []string{"What"}, 0) // substring no longer matches + Assert_GetEntries(t, vault, []string{"johndoe@whatever.com"}, 1) // matches full subtitle + Assert_GetEntries(t, vault, []string{"johndoe"}, 0) // subtitle substring no longer matches + Assert_GetEntries(t, vault, []string{"What", "Whatever"}, 1) // OR: one exact match suffices +} + func TestVault_GetEntries_Filter_Fields(t *testing.T) { vault, err := NewVault(vaultPath, logrus.ErrorLevel) if err != nil {