From ece44daadb639dbd0715431e3317fea5c2ee552f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 14:31:46 +0200 Subject: [PATCH] feat: revealed JSON is shown formatted, and PEM certificates as what they say Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/ui/panes/detail.go | 9 +++- internal/ui/panes/valueview.go | 79 +++++++++++++++++++++++++++++ internal/ui/panes/valueview_test.go | 60 ++++++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 internal/ui/panes/valueview.go create mode 100644 internal/ui/panes/valueview_test.go diff --git a/internal/ui/panes/detail.go b/internal/ui/panes/detail.go index cd2a695..416df35 100644 --- a/internal/ui/panes/detail.go +++ b/internal/ui/panes/detail.go @@ -313,7 +313,8 @@ func (d Detail) lines(st styles.Styles) []string { case ValueShown: // Where the dots were, continuing under itself; the keys below. first := true - for _, line := range strings.Split(d.value.Reveal(), "\n") { + lines, note := valueView(d.value.Reveal()) + for _, line := range lines { for _, w := range strings.Split(ansi.Hardwrap(line, max(1, inner-11), true), "\n") { if first { out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+st.Value.Render(w)) @@ -324,6 +325,12 @@ func (d Detail) lines(st styles.Styles) []string { } } var more []string + if note != "" && d.CopyKey != "" { + note += ": " + d.CopyKey + " copies it as stored" + } + if note != "" { + more = append(more, note) + } if d.hidesIn != "" { more = append(more, "hides in "+d.hidesIn) } diff --git a/internal/ui/panes/valueview.go b/internal/ui/panes/valueview.go new file mode 100644 index 0000000..beae4d3 --- /dev/null +++ b/internal/ui/panes/valueview.go @@ -0,0 +1,79 @@ +package panes + +import ( + "bytes" + "crypto/sha1" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "strings" + "time" +) + +// A revealed value is shown as it reads best: JSON indented, and PEM +// certificates as what they say (subject, issuer, dates, names) rather +// than base64, with any other PEM block, a private key say, named but not +// shown. Anything else is shown as it is. y copies it as stored. + +// valueView is how to show v, and what's been done to it ("" when it's +// shown as it is). +func valueView(v string) (lines []string, note string) { + if lines, ok := pemView(v); ok { + return lines, "certificate" + } + if t := strings.TrimSpace(v); (strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[")) && json.Valid([]byte(t)) { + var b bytes.Buffer + if json.Indent(&b, []byte(t), "", " ") == nil { + return strings.Split(b.String(), "\n"), "JSON, formatted" + } + } + return strings.Split(v, "\n"), "" +} + +// pemView describes the PEM blocks in v, if it's PEM with a certificate. +func pemView(v string) ([]string, bool) { + rest := []byte(strings.TrimSpace(v)) + var out []string + certs := 0 + for len(rest) > 0 { + block, after := pem.Decode(rest) + if block == nil { + return nil, false // text that isn't PEM: show it all + } + rest = bytes.TrimSpace(after) + if len(out) > 0 { + out = append(out, "") + } + if block.Type != "CERTIFICATE" { + out = append(out, block.Type+" (not shown)") + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, false + } + certs++ + out = append(out, certLines(cert)...) + } + return out, certs > 0 +} + +func certLines(c *x509.Certificate) []string { + day := func(t time.Time) string { return t.UTC().Format("2006-01-02") } + out := []string{ + "subject " + c.Subject.String(), + "issuer " + c.Issuer.String(), + "valid " + day(c.NotBefore) + " to " + day(c.NotAfter), + } + names := append([]string{}, c.DNSNames...) + for _, ip := range c.IPAddresses { + names = append(names, ip.String()) + } + names = append(names, c.EmailAddresses...) + if len(names) > 0 { + out = append(out, "names "+strings.Join(names, ", ")) + } + sum := sha1.Sum(c.Raw) // what Azure and Windows call the thumbprint + return append(out, "SHA-1 "+fmt.Sprintf("%X", sum[:])) +} diff --git a/internal/ui/panes/valueview_test.go b/internal/ui/panes/valueview_test.go new file mode 100644 index 0000000..1f2e05b --- /dev/null +++ b/internal/ui/panes/valueview_test.go @@ -0,0 +1,60 @@ +package panes + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "slices" + "strings" + "testing" + "time" +) + +func TestValueViewJSON(t *testing.T) { + lines, note := valueView(`{"user":"app","port":5432,"tags":["a"]}`) + want := []string{"{", ` "user": "app",`, ` "port": 5432,`, ` "tags": [`, ` "a"`, " ]", "}"} + if !slices.Equal(lines, want) || note != "JSON, formatted" { + t.Errorf("lines %q, note %q", lines, note) + } + for _, v := range []string{"plain value", "{not json", "42", "line one\nline two"} { + if lines, note := valueView(v); note != "" || strings.Join(lines, "\n") != v { + t.Errorf("%q shown as %q (%q)", v, lines, note) + } + } +} + +func TestValueViewCertificate(t *testing.T) { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "app.example.com"}, + DNSNames: []string{"app.example.com", "www.app.example.com"}, + NotBefore: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), NotAfter: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + keyDER, _ := x509.MarshalECPrivateKey(key) + bundle := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) + + string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})) + + lines, note := valueView(bundle) + text := strings.Join(lines, "\n") + for _, want := range []string{"subject CN=app.example.com", "valid 2026-01-01 to 2027-01-01", + "names app.example.com, www.app.example.com", "SHA-1 ", "EC PRIVATE KEY (not shown)"} { + if !strings.Contains(text, want) { + t.Errorf("missing %q:\n%s", want, text) + } + } + if note != "certificate" || strings.Contains(text, "BEGIN") { + t.Errorf("note %q, or PEM shown:\n%s", note, text) + } + // A private key alone isn't a certificate: shown as it is. + if _, note := valueView(string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))); note != "" { + t.Errorf("a key alone: note %q", note) + } +}