@@ -11,6 +11,7 @@ import (
1111 "sort"
1212 "strconv"
1313 "strings"
14+ "time"
1415
1516 "github.com/gdamore/tcell/v2"
1617 "github.com/hazcod/enpass-cli/pkg/clipboard"
@@ -126,8 +127,8 @@ func printHelp() {
126127 fmt .Println ("Usage: enpass-cli [flags] <command> [filters...]" )
127128 fmt .Println ()
128129 fmt .Println ("Commands:" )
129- fmt .Println (" list [filter] List entries (without passwords)" )
130- fmt .Println (" show [filter] Show entries (with passwords)" )
130+ fmt .Println (" list [filter] List entries (without passwords; TOTP fields masked )" )
131+ fmt .Println (" show [filter] Show entries (with passwords; computes RFC 6238 TOTP code )" )
131132 fmt .Println (" copy <filter> Copy password to clipboard" )
132133 fmt .Println (" pass <filter> Print password to stdout" )
133134 fmt .Println (" ui Interactive terminal UI" )
@@ -140,6 +141,11 @@ func printHelp() {
140141 fmt .Println (" version Print version" )
141142 fmt .Println (" help Print this help" )
142143 fmt .Println ()
144+ fmt .Println ("Pass -detailed to list/show to see every field of each entry instead of" )
145+ fmt .Println ("only the summary fields (title, login, category, label, type). TOTP fields" )
146+ fmt .Println ("are treated as sensitive: their secret is hidden in list, and show prints" )
147+ fmt .Println ("the current RFC 6238 code alongside the secret." )
148+ fmt .Println ()
143149 fmt .Println ("Flags:" )
144150 flag .Usage ()
145151}
@@ -183,12 +189,17 @@ type entryView struct {
183189
184190// fieldView is a single field of an entry (username, email, password, ...).
185191// Value is empty when the field is sensitive and the caller didn't ask for
186- // decrypted output (list mode).
192+ // decrypted output (list mode). For TOTP fields the stored Value is the
193+ // secret key, so it's treated as sensitive: hidden in list mode, included in
194+ // show mode. TOTPCode carries the current RFC 6238 code; TOTPError is set
195+ // when computing it failed.
187196type fieldView struct {
188197 Type string `json:"type"`
189198 Label string `json:"label,omitempty"`
190199 Sensitive bool `json:"sensitive,omitempty"`
191200 Value string `json:"value,omitempty"`
201+ TOTPCode string `json:"totp_code,omitempty"`
202+ TOTPError string `json:"totp_error,omitempty"`
192203}
193204
194205// collectEntries fetches every field for matching entries and groups them by
@@ -219,6 +230,18 @@ func collectEntries(vault *enpass.Vault, args *Args, includeSensitive bool) ([]e
219230 if c .IsTrashed () && ! * args .trashed {
220231 continue
221232 }
233+ // Non-password field values are stored in cleartext; Decrypt() returns
234+ // them as-is. For password fields, Decrypt() actually decrypts.
235+ value , derr := c .Decrypt ()
236+ if derr != nil {
237+ return nil , fmt .Errorf ("could not decrypt %s/%s: %w" , c .Title , c .Label , derr )
238+ }
239+ // Match the Enpass native apps' view mode: hide empty-value template
240+ // placeholders that a user never filled in (e.g. "Date Mod", "Field 6").
241+ // Sections are visual dividers and stay even when empty.
242+ if value == "" && c .Type != "section" {
243+ continue
244+ }
222245 g , ok := groups [c .UUID ]
223246 if ! ok {
224247 g = & entryView {
@@ -236,13 +259,22 @@ func collectEntries(vault *enpass.Vault, args *Args, includeSensitive bool) ([]e
236259 Label : c .Label ,
237260 Sensitive : c .Sensitive ,
238261 }
239- // Non-password field values are stored in cleartext; Decrypt() returns
240- // them as-is. For password fields, Decrypt() actually decrypts.
241- value , derr := c .Decrypt ()
242- if derr != nil {
243- return nil , fmt .Errorf ("could not decrypt %s/%s: %w" , c .Title , c .Label , derr )
262+ isTOTP := c .Type == "totp"
263+ hasValue := value != ""
264+ // TOTP fields are classified as sensitive: in list mode neither the
265+ // secret nor the live code is exposed. Only compute the code when the
266+ // caller is going to display it.
267+ if isTOTP && hasValue && includeSensitive {
268+ if code , terr := enpass .ComputeTOTP (value , time .Now ()); terr == nil {
269+ f .TOTPCode = code
270+ } else {
271+ f .TOTPError = terr .Error ()
272+ }
273+ }
274+ if isTOTP && hasValue {
275+ f .Sensitive = true
244276 }
245- if includeSensitive || ! c .Sensitive {
277+ if includeSensitive || ! f .Sensitive {
246278 f .Value = value
247279 }
248280 g .Fields = append (g .Fields , f )
@@ -346,22 +378,65 @@ func outputDetailed(logger *logrus.Logger, entries []entryView, args *Args) {
346378 if name == "" {
347379 name = f .Type
348380 }
381+ // Three-level hierarchy: record header (no indent), section header
382+ // (4 spaces), regular field (8 spaces). Regular fields are at the
383+ // same depth whether the record has sections or not, so columns
384+ // stay aligned across records.
385+ indent := fieldIndent
386+ if f .Type == "section" {
387+ indent = sectionIndent
388+ }
389+ if f .Type == "totp" && (f .TOTPCode != "" || f .TOTPError != "" ) {
390+ renderTOTPField (logger , indent , name , f )
391+ continue
392+ }
349393 switch {
350394 case f .Sensitive && f .Value == "" :
351- logger .Printf (" %s (%s): ********" , name , f .Type )
395+ logger .Printf ("%s%s (%s): ********" , indent , name , f .Type )
352396 case f .Value != "" :
353- logger .Printf (" %s (%s): %s" , name , f .Type , f .Value )
397+ logger .Printf ("%s%s (%s): %s" , indent , name , f .Type , f .Value )
354398 default :
355- logger .Printf (" %s (%s)" , name , f .Type )
399+ logger .Printf ("%s%s (%s)" , indent , name , f .Type )
356400 }
357401 }
358402 }
359403}
360404
405+ const (
406+ sectionIndent = " "
407+ fieldIndent = " "
408+ )
409+
410+ // renderTOTPField prints a TOTP field. When the code could be computed we
411+ // show it; otherwise we tell the user the value is dynamic. The secret is
412+ // only included when collectEntries chose to expose it (i.e. show mode).
413+ func renderTOTPField (logger * logrus.Logger , indent , name string , f fieldView ) {
414+ parts := []string {}
415+ switch {
416+ case f .TOTPCode != "" :
417+ parts = append (parts , "code " + f .TOTPCode )
418+ case f .TOTPError != "" :
419+ parts = append (parts , "<dynamic TOTP value>" )
420+ default :
421+ logger .Printf ("%s%s (%s)" , indent , name , f .Type )
422+ return
423+ }
424+ if f .Value != "" {
425+ parts = append (parts , "secret: " + f .Value )
426+ }
427+ logger .Printf ("%s%s (%s): %s" , indent , name , f .Type , strings .Join (parts , " " ))
428+ }
429+
361430// anchorField picks the field that represents the entry in compact mode.
362- // Mirrors the original GetEntries dedup: prefer the sensitive (password)
363- // field, fall back to the first field.
431+ // Prefer the password field so the compact summary stays password-focused
432+ // even when other sensitive field types (e.g. TOTP) are present. Fall back
433+ // to any sensitive field, then to the first field.
364434func anchorField (fields []fieldView ) * fieldView {
435+ for i := range fields {
436+ if fields [i ].Type == "password" {
437+ return & fields [i ]
438+ }
439+ }
365440 for i := range fields {
366441 if fields [i ].Sensitive {
367442 return & fields [i ]
0 commit comments