Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 188 additions & 50 deletions cmd/enpasscli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
pinEnable *bool
sort *bool
trashed *bool
detailed *bool
and *bool
clipboardPrimary *bool
// write command flags
Expand All @@ -91,6 +92,7 @@
args.and = flag.Bool("and", false, "Combines filters with AND instead of default OR.")
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.")
args.clipboardPrimary = flag.Bool("clipboardPrimary", false, "Use primary X selection instead of clipboard for the 'copy' command.")
// write command flags
args.title = flag.String("title", "", "Entry title (for create/edit).")
Expand Down Expand Up @@ -154,85 +156,221 @@
}

func listEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
cards, err := vault.GetEntries(*args.cardType, args.filters)
entries, err := collectEntries(vault, args, false)
if err != nil {
logger.WithError(err).Fatal("could not retrieve cards")
}
if *args.sort {
sortEntries(cards)
logger.WithError(err).Fatal(err.Error())
}
outputEntriesOrLog(logger, entries, args)
}

data, err := prepareCardData(cards, false, args)
func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
entries, err := collectEntries(vault, args, true)
if err != nil {
logger.WithError(err).Fatal(err.Error())
}
outputEntriesOrLog(logger, entries, args)
}

outputDataOrLog(logger, data, args)
// entryView is one Enpass item with all of its fields grouped together.
type entryView struct {
UUID string `json:"uuid"`
Title string `json:"title"`
Subtitle string `json:"subtitle,omitempty"`
Category string `json:"category,omitempty"`
Trashed bool `json:"trashed,omitempty"`
Fields []fieldView `json:"fields"`
}

func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
cards, err := vault.GetEntries(*args.cardType, args.filters)
// fieldView is a single field of an entry (username, email, password, ...).
// Value is empty when the field is sensitive and the caller didn't ask for
// decrypted output (list mode).
type fieldView struct {
Type string `json:"type"`
Label string `json:"label,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
Value string `json:"value,omitempty"`
}

// collectEntries fetches every field for matching entries and groups them by
// item UUID. When includeSensitive is false, values of sensitive fields
// (passwords) are omitted while non-sensitive fields like username/email are
// still populated — this is what powers the "list shows usernames and emails
// but not passwords" behavior.
func collectEntries(vault *enpass.Vault, args *Args, includeSensitive bool) ([]entryView, error) {
// The -type flag defaults to "password" for the copy/pass commands. For
// list/show we want every field type, so treat the default as "no filter".
// Any other explicit value still filters server-side.
typeFilter := *args.cardType
if typeFilter == "password" {
typeFilter = ""
}

cards, err := vault.GetAllFields(typeFilter, args.filters)
if err != nil {
logger.WithError(err).Fatal("could not retrieve cards")
}
if *args.sort {
sortEntries(cards)
return nil, fmt.Errorf("could not retrieve cards: %w", err)
}

data, err := prepareCardData(cards, true, args)
if err != nil {
logger.WithError(err).Fatal(err.Error())
order := make([]string, 0)
groups := make(map[string]*entryView)
for _, c := range cards {
if c.IsDeleted() {
continue
}
if c.IsTrashed() && !*args.trashed {
continue
}
g, ok := groups[c.UUID]
if !ok {
g = &entryView{
UUID: c.UUID,
Title: c.Title,
Subtitle: c.Subtitle,
Category: c.Category,
Trashed: c.IsTrashed(),
}
groups[c.UUID] = g
order = append(order, c.UUID)
}
f := fieldView{
Type: c.Type,
Label: c.Label,
Sensitive: c.Sensitive,
}
// Non-password field values are stored in cleartext; Decrypt() returns
// them as-is. For password fields, Decrypt() actually decrypts.
value, derr := c.Decrypt()
if derr != nil {
return nil, fmt.Errorf("could not decrypt %s/%s: %w", c.Title, c.Label, derr)
}
if includeSensitive || !c.Sensitive {
f.Value = value
}
g.Fields = append(g.Fields, f)
}

outputDataOrLog(logger, data, args)
entries := make([]entryView, 0, len(order))
for _, uuid := range order {
entries = append(entries, *groups[uuid])
}
if *args.sort {
sort.SliceStable(entries, func(i, j int) bool {
return strings.ToLower(entries[i].Title) < strings.ToLower(entries[j].Title)
})
}
return entries, nil
}

func prepareCardData(cards []enpass.Card, includeDecrypted bool, args *Args) ([]map[string]string, error) {
data := make([]map[string]string, 0)
for _, card := range cards {
if card.IsTrashed() && !*args.trashed {
continue
}
func outputEntriesOrLog(logger *logrus.Logger, entries []entryView, args *Args) {
if *args.detailed {
outputDetailed(logger, entries, args)
return
}
outputCompact(logger, entries, args)
}

cardMap := map[string]string{
"title": card.Title,
"login": card.Subtitle,
"category": card.Category,
"label": card.Label,
"type": card.Type,
// outputCompact reproduces the original list/show output: one row per entry
// with the summary fields title, login, category, label, type — plus password
// when present (show mode).
func outputCompact(logger *logrus.Logger, entries []entryView, args *Args) {
type compactRow struct {
Title string `json:"title"`
Login string `json:"login"`
Category string `json:"category"`
Label string `json:"label"`
Type string `json:"type"`
Password string `json:"password,omitempty"`
}

rows := make([]compactRow, 0, len(entries))
for _, e := range entries {
anchor := anchorField(e.Fields)
row := compactRow{
Title: e.Title,
Login: e.Subtitle,
Category: e.Category,
}

if includeDecrypted {
decrypted, err := card.Decrypt()
if err != nil {
return nil, fmt.Errorf("could not decrypt %s: %w", card.Title, err)
if anchor != nil {
row.Label = anchor.Label
row.Type = anchor.Type
if anchor.Sensitive {
row.Password = anchor.Value
}
cardMap["password"] = decrypted
}
rows = append(rows, row)
}

data = append(data, cardMap)
if *args.jsonOutput {
jsonData, err := json.Marshal(rows)
if err != nil {
logger.WithError(err).Fatal("could not marshal JSON data")
}
fmt.Println(string(jsonData))

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to Password
flows to a logging call.
return
}
for _, r := range rows {
format := "> title: %s login: %s cat.: %s label: %s type: %s"
vals := []any{r.Title, r.Login, r.Category, r.Label, r.Type}
if r.Password != "" {
format += " password: %s"
vals = append(vals, r.Password)
}
logger.Printf(format, vals...)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to Password
flows to a logging call.
}
return data, nil
}

func outputDataOrLog(logger *logrus.Logger, data []map[string]string, args *Args) {
// outputDetailed emits the grouped per-field view: one header line per entry
// followed by an indented line per field.
func outputDetailed(logger *logrus.Logger, entries []entryView, args *Args) {
if *args.jsonOutput {
jsonData, jsonErr := json.Marshal(data)
if jsonErr != nil {
logger.WithError(jsonErr).Fatal("could not marshal JSON data")
jsonData, err := json.Marshal(entries)
if err != nil {
logger.WithError(err).Fatal("could not marshal JSON data")
}
fmt.Println(string(jsonData))
} else {
for _, card := range data {
logger.Printf(
"> title: %s login: %s cat.: %s label: %s",
card["title"],
card["login"],
card["category"],
card["label"],
)
return
}
for _, e := range entries {
header := "> " + e.Title
if e.Subtitle != "" {
header += " (" + e.Subtitle + ")"
}
if e.Category != "" {
header += " cat.: " + e.Category
}
if e.Trashed {
header += " [trashed]"
}
logger.Print(header)
for _, f := range e.Fields {
name := f.Label
if name == "" {
name = f.Type
}
switch {
case f.Sensitive && f.Value == "":
logger.Printf(" %s (%s): ********", name, f.Type)
case f.Value != "":
logger.Printf(" %s (%s): %s", name, f.Type, f.Value)
default:
logger.Printf(" %s (%s)", name, f.Type)
}
}
}
}

// anchorField picks the field that represents the entry in compact mode.
// Mirrors the original GetEntries dedup: prefer the sensitive (password)
// field, fall back to the first field.
func anchorField(fields []fieldView) *fieldView {
for i := range fields {
if fields[i].Sensitive {
return &fields[i]
}
}
if len(fields) > 0 {
return &fields[0]
}
return nil
}

func copyEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
Expand Down
37 changes: 37 additions & 0 deletions pkg/enpass/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,43 @@ func (v *Vault) GetEntries(cardType string, filters []string) ([]Card, error) {
return cards, nil
}

// GetAllFields returns every itemfield row matching the filters, without
// deduplicating by item UUID. Each returned Card represents a single field
// (e.g. username, email, password) belonging to an entry. Use this when the
// caller wants to display or operate on multiple fields per entry; use
// GetEntries when the caller wants one Card per entry.
func (v *Vault) GetAllFields(cardType string, filters []string) ([]Card, error) {
if v.db == nil || v.vaultInfo.VaultName == "" {
return nil, errors.New("vault is not initialized")
}

rows, err := v.executeEntryQuery(cardType, filters)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve cards from database")
}
defer rows.Close()

cards := make([]Card, 0)
for rows.Next() {
var card Card
if err := rows.Scan(
&card.UUID, &card.Type, &card.CreatedAt, &card.UpdatedAt, &card.Title,
&card.Subtitle, &card.Note, &card.Trashed, &card.Deleted, &card.Category,
&card.Label, &card.value, &card.itemKey, &card.LastUsed, &card.Sensitive, &card.Icon,
); err != nil {
return nil, errors.Wrap(err, "could not read card from database")
}
card.RawValue = card.value
cards = append(cards, card)
}

if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "error iterating database rows")
}

return cards, nil
}

func (v *Vault) GetEntry(cardType string, filters []string, unique bool) (*Card, error) {
cards, err := v.GetEntries(cardType, filters)
if err != nil {
Expand Down
Loading