feat: decrypt Jasypt values in .env and Spring files #80

Merged
keb merged 1 commits from feat/jasypt into main 2026-10-15 13:31:58 +02:00
13 changed files with 381 additions and 29 deletions
Showing only changes of commit e1f86c72f7 - Show all commits

No files matched your search

+22 -5
View File
@@ -104,11 +104,12 @@ they are.
## Decrypting ## Decrypting
Listing never decrypts: SOPS encrypts values only, so keys and metadata Listing never decrypts: SOPS and Jasypt encrypt values only, so keys and
(encryption, last modified) are read straight from the files. Revealing, metadata (encryption, last modified) are read straight from the files.
copying or `lazysecrets get` decrypts one value with your `sops` binary, so Revealing, copying or `lazysecrets get` decrypts one value; SOPS's with
whatever sops uses works (age keys, Azure Key Vault through az, AWS/GCP your `sops` binary, so whatever sops uses works (age keys, Azure Key Vault
KMS, PGP). When sops can't decrypt, its whole error message is shown. through az, AWS/GCP KMS, PGP). When sops can't decrypt, its whole error
message is shown.
On a file, the right pane shows how it's encrypted and the keys it's On a file, the right pane shows how it's encrypted and the keys it's
encrypted for (public: age recipients, Key Vault keys, KMS ARNs, PGP encrypted for (public: age recipients, Key Vault keys, KMS ARNs, PGP
@@ -117,6 +118,22 @@ tracked, ignored or neither, and its last commit. A plain `.env` file that
git tracks, or that isn't ignored, is marked in red: its values are in the git tracks, or that isn't ignored, is marked in red: its values are in the
repo's history, or easily could be. repo's history, or easily could be.
### Jasypt
A value like `ENC(...)` in a plain `.env` or Spring file is
[Jasypt](http://www.jasypt.org)'s, decrypted when it's revealed or copied
with jasypt-spring-boot's default algorithm (3.0+:
`PBEWITHHMACSHA512ANDAES_256`). The password is, as in Spring:
1. `$JASYPT_ENCRYPTOR_PASSWORD`,
2. the file's own: `jasypt.encryptor.password` in a Spring file,
`JASYPT_ENCRYPTOR_PASSWORD` in a `.env`; a `${VAR}` (or
`${VAR:default}`) in it is read from the environment.
Environment variables are lazysecrets' own: a link opened from the browser
starts it without your shell's. A `.env` whose values are all Jasypt's
isn't marked in red.
## Opening files ## Opening files
`e` opens the selected file, or on a secret, its file at that key's line: `e` opens the selected file, or on a secret, its file at that key's line:
+67
View File
@@ -0,0 +1,67 @@
// Package jasypt decrypts values encrypted with Jasypt the way
// jasypt-spring-boot 3.0 and later does by default: ENC(...) around the
// base64 of a random salt, a random IV and the ciphertext, with
// PBEWITHHMACSHA512ANDAES_256 (PBKDF2-HMAC-SHA512, 1000 iterations,
// AES-256-CBC).
package jasypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/pbkdf2"
"crypto/sha512"
"encoding/base64"
"errors"
"strings"
"unicode/utf8"
)
const iterations = 1000
// ErrDecrypt is a value that doesn't decrypt with the password: a wrong
// password, or one encrypted with another algorithm.
var ErrDecrypt = errors.New("doesn't decrypt: a wrong password, or not Jasypt's default algorithm (PBEWITHHMACSHA512ANDAES_256)")
// Is reports whether v is a Jasypt value, ENC(...).
func Is(v string) bool {
return len(v) > len("ENC()") && strings.HasPrefix(v, "ENC(") && strings.HasSuffix(v, ")")
}
// Decrypt decrypts an ENC(...) value.
func Decrypt(v, password string) (string, error) {
b, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(strings.TrimPrefix(v, "ENC("), ")"))
if err != nil {
return "", errors.New("not Jasypt: ENC(...) isn't base64")
}
const size = aes.BlockSize // the salt's and the IV's too
if len(b) < 3*size || len(b)%size != 0 {
return "", ErrDecrypt
}
salt, iv, data := b[:size], b[size:2*size], b[2*size:]
key, err := pbkdf2.Key(sha512.New, password, salt, iterations, 32)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
plain := make([]byte, len(data))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, data)
// A wrong password decrypts to noise: its padding is almost never
// valid, and the rest rarely UTF-8.
n := int(plain[len(plain)-1])
if n == 0 || n > size {
return "", ErrDecrypt
}
for _, c := range plain[len(plain)-n:] {
if int(c) != n {
return "", ErrDecrypt
}
}
plain = plain[:len(plain)-n]
if !utf8.Valid(plain) {
return "", ErrDecrypt
}
return string(plain), nil
}
+37
View File
@@ -0,0 +1,37 @@
package jasypt
import (
"errors"
"testing"
)
// Encrypted by Jasypt 1.9.3 itself, set up as jasypt-spring-boot's default
// encryptor, with the password "test-password".
var vectors = map[string]string{
"ENC(5IQUKT4B7j38TnZJ+BlWqo/zSkKHTjl4XRJyaQEHgkCy4r00avLt4dWqFB74X0Zw)": "hunter2",
"ENC(w4pm2JVZcbG5R+Ud9uKHg8VhJ/0aAK7sFXtNBv+Vo450SAchTQtfhw0TnIJ9gN+D0VFbwqJzfXU369coTJz2rw==)": "blåbærsyltetøy ✓",
}
func TestDecrypt(t *testing.T) {
for enc, want := range vectors {
if !Is(enc) {
t.Errorf("%s: not Jasypt", enc)
}
if got, err := Decrypt(enc, "test-password"); err != nil || got != want {
t.Errorf("%s: %q %v, want %q", enc, got, err, want)
}
if _, err := Decrypt(enc, "wrong"); !errors.Is(err, ErrDecrypt) {
t.Errorf("%s with a wrong password: %v", enc, err)
}
}
for _, v := range []string{"ENC(not base64!)", "ENC(aGVsbG8=)"} {
if _, err := Decrypt(v, "test-password"); err == nil {
t.Errorf("%s: no error", v)
}
}
for _, v := range []string{"ENC()", "hunter2", "ENC[AES256_GCM,data:x]", "${DB_PASSWORD}"} {
if Is(v) {
t.Errorf("%s: Jasypt", v)
}
}
}
+1
View File
@@ -133,6 +133,7 @@ type Object struct {
Type ObjectType Type ObjectType
ID string ID string
Name string Name string
Detail string // shown after the name, e.g. "jasypt" for how its value is kept
Version string Version string
Enabled *bool Enabled *bool
Updated time.Time Updated time.Time
+30 -3
View File
@@ -2,6 +2,8 @@ package repo
import ( import (
"context" "context"
"fmt"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -22,8 +24,8 @@ func (p *Provider) Details(ctx context.Context, c provider.Container) ([]provide
} }
var out []provider.Attr var out []provider.Attr
meta, isSops := readMeta(c.ID) meta, isSops := readMeta(c.ID)
// A plain file's values are at risk in git; a Spring file's only when // A plain file's values are at risk in git, unless they're all Jasypt;
// one of its secrets is in plain text. // a Spring file's only when one of its secrets is in plain text.
plain := !isSops plain := !isSops
if !isSops && IsSpring(c.ID) { if !isSops && IsSpring(c.ID) {
out, plain = p.springDetails(c) out, plain = p.springDetails(c)
@@ -40,7 +42,7 @@ func (p *Provider) Details(ctx context.Context, c provider.Container) ([]provide
out = append(out, provider.Attr{Label: "encrypted", Value: meta.modified.Local().Format("2006-01-02 15:04")}) out = append(out, provider.Attr{Label: "encrypted", Value: meta.modified.Local().Format("2006-01-02 15:04")})
} }
} else { } else {
out = append(out, provider.Attr{Label: "encryption", Value: "none: plain text"}) out, plain = envDetails(c.ID)
} }
git := func(args ...string) (string, bool) { git := func(args ...string) (string, bool) {
@@ -65,3 +67,28 @@ func (p *Provider) Details(ctx context.Context, c provider.Container) ([]provide
} }
return out, nil return out, nil
} }
// envDetails says how a plain env file's values are encrypted, and whether
// any is in plain text.
func envDetails(file string) (attrs []provider.Attr, plain bool) {
b, err := os.ReadFile(file)
if err != nil {
return []provider.Attr{{Label: "encryption", Value: err.Error(), Warn: true}}, true
}
keys, _ := parseKeys(file, b)
n := 0
for _, k := range keys {
if k.jasypt {
n++
}
}
return []provider.Attr{{Label: "encryption", Value: jasyptShare(n, len(keys))}}, n < len(keys)
}
// jasyptShare is how many of a plain file's values are Jasypt's.
func jasyptShare(n, of int) string {
if n == 0 {
return "none: plain text"
}
return fmt.Sprintf("jasypt: %d of %d values", n, of)
}
+74
View File
@@ -0,0 +1,74 @@
package repo
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"git.kebcloud.no/keb/lazysecrets/internal/jasypt"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
)
// jasyptEnv is where Spring looks for the password first, before the
// config's jasypt.encryptor.password, and so does lazysecrets.
const jasyptEnv = "JASYPT_ENCRYPTOR_PASSWORD"
// jasyptValue decrypts v when it's a Jasypt value, ENC(...), from a plain
// file; other values are as they are.
func jasyptValue(file string, v provider.SecretValue) (provider.SecretValue, error) {
s := v.Reveal()
if !jasypt.Is(s) {
return v, nil
}
password, from, err := jasyptPassword(file)
if err != nil {
return provider.SecretValue{}, err
}
plain, err := jasypt.Decrypt(s, password)
if errors.Is(err, jasypt.ErrDecrypt) {
return provider.SecretValue{}, fmt.Errorf("jasypt: with the password from %s: %w", from, err)
}
if err != nil {
return provider.SecretValue{}, fmt.Errorf("jasypt: %w", err)
}
return provider.NewSecretValue([]byte(plain)), nil
}
// jasyptPassword is the password for a file's Jasypt values, and where it's
// from: $JASYPT_ENCRYPTOR_PASSWORD, else the file's own setting
// (jasypt.encryptor.password in a Spring file, JASYPT_ENCRYPTOR_PASSWORD
// in an env file), with a ${VAR} in it read from the environment.
func jasyptPassword(file string) (password, from string, err error) {
if v := os.Getenv(jasyptEnv); v != "" {
return v, "$" + jasyptEnv, nil
}
setting, read := jasyptEnv, plainValue
if IsSpring(file) {
setting, read = "jasypt.encryptor.password", springValue
}
v, err := read(file, setting)
if err != nil {
return "", "", fmt.Errorf("jasypt: no password: set $%s, or %s in %s", jasyptEnv, setting, filepath.Base(file))
}
return resolvePlaceholder(v.Reveal(), setting+" in "+filepath.Base(file))
}
// placeholder is Spring's ${VAR} or ${VAR:default}.
var placeholder = regexp.MustCompile(`^\$\{([^:}]+)(:[^}]*)?\}$`)
// resolvePlaceholder reads a ${VAR} value from the environment, falling
// back to its default; other values are as they are.
func resolvePlaceholder(v, from string) (string, string, error) {
m := placeholder.FindStringSubmatch(v)
switch {
case m == nil:
return v, from, nil
case os.Getenv(m[1]) != "":
return os.Getenv(m[1]), "$" + m[1] + " (" + from + ")", nil
case m[2] != "":
return m[2][1:], "the default in " + from, nil
}
return "", "", fmt.Errorf("jasypt: no password: %s is %s, and $%s isn't set", from, v, m[1])
}
+102
View File
@@ -0,0 +1,102 @@
package repo
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
)
// hunter2, encrypted by Jasypt with the password "test-password".
const jasyptHunter2 = "ENC(5IQUKT4B7j38TnZJ+BlWqo/zSkKHTjl4XRJyaQEHgkCy4r00avLt4dWqFB74X0Zw)"
func TestJasyptPassword(t *testing.T) {
dir := t.TempDir()
value := func(name, body string) (string, error) {
file := filepath.Join(dir, name)
if err := os.WriteFile(file, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
key := "app.token"
if !IsSpring(name) {
key = "TOKEN"
}
v, err := (&Provider{}).Value(context.Background(), provider.Object{ID: file + "\x00" + key, Name: key})
return v.Reveal(), err
}
t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "")
t.Setenv("MY_PASSWORD", "")
spring := func(password string) string {
return "jasypt.encryptor.password: " + password + "\napp.token: " + jasyptHunter2 + "\n"
}
for _, c := range []struct{ name, file, body, env, want string }{
{"the file's own", "application.yml", spring("test-password"), "", "hunter2"},
{"a ${VAR}", "application.yml", spring("${MY_PASSWORD}"), "test-password", "hunter2"},
{"a ${VAR}'s default", "application.yml", spring("${MY_PASSWORD:test-password}"), "", "hunter2"},
{"properties", "application-local.properties", "jasypt.encryptor.password=test-password\napp.token=" + jasyptHunter2 + "\n", "", "hunter2"},
{"env file", ".env", "JASYPT_ENCRYPTOR_PASSWORD=test-password\nTOKEN=" + jasyptHunter2 + "\n", "", "hunter2"},
{"not jasypt", ".env", "TOKEN=hunter2\n", "", "hunter2"},
} {
t.Setenv("MY_PASSWORD", c.env)
if got, err := value(c.file, c.body); err != nil || got != c.want {
t.Errorf("%s: %q %v", c.name, got, err)
}
}
// $JASYPT_ENCRYPTOR_PASSWORD wins over the file's, as in Spring.
t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "test-password")
if got, err := value("application.yml", spring("wrong")); err != nil || got != "hunter2" {
t.Errorf("environment first: %q %v", got, err)
}
t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "wrong")
if _, err := value("application.yml", spring("test-password")); err == nil || !strings.Contains(err.Error(), "the password from $JASYPT_ENCRYPTOR_PASSWORD") {
t.Errorf("a wrong one says where it's from: %v", err)
}
t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "")
t.Setenv("MY_PASSWORD", "")
for body, want := range map[string]string{
spring("${MY_PASSWORD}"): "$MY_PASSWORD isn't set",
"app.token: " + jasyptHunter2: "set $JASYPT_ENCRYPTOR_PASSWORD, or jasypt.encryptor.password in application.yml",
} {
if _, err := value("application.yml", body); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("no password: %v, want %q", err, want)
}
}
}
func TestJasyptEnvFile(t *testing.T) {
root := gitRepo(t, map[string]string{
"jasypt.env": "TOKEN=" + jasyptHunter2 + "\n",
"mixed.env": "TOKEN=" + jasyptHunter2 + "\nURL=https://example.com\n",
})
ctx := context.Background()
p := New(Settings{Dir: root})
srcs, _ := p.Sources(ctx)
cs, _ := p.Containers(ctx, srcs[0])
for _, c := range cs {
attrs, _ := p.Details(ctx, c)
got := map[string]provider.Attr{}
for _, a := range attrs {
got[a.Label] = a
}
switch c.Name {
case "jasypt.env":
if got["encryption"].Value != "jasypt: 1 of 1 values" || got["git"].Warn {
t.Errorf("all jasypt: %+v", got)
}
keys, _ := p.Objects(ctx, c, provider.TypeSecret)
if len(keys) != 1 || keys[0].Attrs[1].Value != "jasypt" || keys[0].Detail != "jasypt" {
t.Errorf("keys: %+v", keys)
}
case "mixed.env":
if got["encryption"].Value != "jasypt: 1 of 2 values" || !got["git"].Warn {
t.Errorf("mixed: %+v", got)
}
}
}
}
+17 -6
View File
@@ -32,6 +32,7 @@ import (
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
"git.kebcloud.no/keb/lazysecrets/internal/jasypt"
"git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider"
) )
@@ -380,8 +381,11 @@ func (p *Provider) Objects(_ context.Context, c provider.Container, t provider.O
Container: c, Type: provider.TypeSecret, Container: c, Type: provider.TypeSecret,
ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, Attrs: attrs, ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, Attrs: attrs,
} }
if isSops && !k.encrypted { switch {
case isSops && !k.encrypted:
o.Attrs = append(slices.Clip(attrs), provider.Attr{Label: "encrypted", Value: "no (left in plain text by the file's sops rules)"}) o.Attrs = append(slices.Clip(attrs), provider.Attr{Label: "encrypted", Value: "no (left in plain text by the file's sops rules)"})
case !isSops && k.jasypt:
o.Attrs, o.Detail = []provider.Attr{attrs[0], {Label: "encrypted", Value: "jasypt"}}, "jasypt"
} }
out = append(out, o) out = append(out, o)
} }
@@ -396,17 +400,23 @@ func (p *Provider) Versions(context.Context, provider.Object) ([]provider.Object
return nil, provider.ErrUnsupported return nil, provider.ErrUnsupported
} }
// Value decrypts one key with sops. // Value reads one key: from a plain file, decrypting a Jasypt value; from
// a SOPS file, with sops.
func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) { func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) {
file, extract, ok := strings.Cut(o.ID, "\x00") file, extract, ok := strings.Cut(o.ID, "\x00")
if !ok { // made from a link: the repo's root, the file's path, the key if !ok { // made from a link: the repo's root, the file's path, the key
file = filePath(o.Container) file = filePath(o.Container)
} }
if _, isSops := readMeta(file); !isSops { if _, isSops := readMeta(file); !isSops {
read := plainValue
if IsSpring(file) { if IsSpring(file) {
return springValue(file, o.Name) read = springValue
} }
return plainValue(file, o.Name) v, err := read(file, o.Name)
if err != nil {
return v, err
}
return jasyptValue(file, v)
} }
if !ok { if !ok {
var err error var err error
@@ -1045,7 +1055,8 @@ type key struct {
name string // db.password, list[0], with . [ ] \ in keys escaped name string // db.password, list[0], with . [ ] \ in keys escaped
extract string // sops --extract: ["db"]["password"] extract string // sops --extract: ["db"]["password"]
encrypted bool encrypted bool
line int // 1-based jasypt bool // a plain env file's ENC(...) value
line int // 1-based
} }
// parseKeys lists a sops file's keys without decrypting anything. // parseKeys lists a sops file's keys without decrypting anything.
@@ -1070,7 +1081,7 @@ func parseKeys(file string, b []byte) ([]key, error) {
continue continue
} }
sopsKey, _, _ := strings.Cut(line, "=") // sops keeps export and spaces in the key sopsKey, _, _ := strings.Cut(line, "=") // sops keeps export and spaces in the key
out = append(out, key{name: k, extract: quote(sopsKey), encrypted: strings.HasPrefix(v, "ENC["), line: i + 1}) out = append(out, key{name: k, extract: quote(sopsKey), encrypted: strings.HasPrefix(v, "ENC["), jasypt: jasypt.Is(v), line: i + 1})
} }
return out, nil return out, nil
case "ini": case "ini":
+7 -10
View File
@@ -15,6 +15,7 @@ import (
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
"git.kebcloud.no/keb/lazysecrets/internal/jasypt"
"git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider"
) )
@@ -137,7 +138,7 @@ func (k springKey) reference() bool {
} }
func (k springKey) jasypt() bool { func (k springKey) jasypt() bool {
return strings.HasPrefix(k.value, "ENC(") && strings.HasSuffix(k.value, ")") return jasypt.Is(k.value)
} }
// plain reports whether the file holds the value itself, readable. // plain reports whether the file holds the value itself, readable.
@@ -346,15 +347,15 @@ func (p *Provider) springObjects(c provider.Container, b []byte) ([]provider.Obj
if !filter.secret(k) { if !filter.secret(k) {
continue continue
} }
state := provider.Attr{Label: "encrypted", Value: "no: plain text in the file"} state, detail := provider.Attr{Label: "encrypted", Value: "no: plain text in the file"}, ""
switch { switch {
case k.reference(): case k.reference():
state = provider.Attr{Label: "value", Value: "a reference: the secret is set elsewhere"} state, detail = provider.Attr{Label: "value", Value: "a reference: the secret is set elsewhere"}, "reference"
case k.jasypt(): case k.jasypt():
state = provider.Attr{Label: "encrypted", Value: "jasypt"} state, detail = provider.Attr{Label: "encrypted", Value: "jasypt"}, "jasypt"
} }
out = append(out, provider.Object{ out = append(out, provider.Object{
Container: c, Type: provider.TypeSecret, ID: c.ID + "\x00" + k.name, Name: k.name, Container: c, Type: provider.TypeSecret, ID: c.ID + "\x00" + k.name, Name: k.name, Detail: detail,
Attrs: []provider.Attr{format, state}, Attrs: []provider.Attr{format, state},
}) })
} }
@@ -403,12 +404,8 @@ func (p *Provider) springDetails(c provider.Container) (attrs []provider.Attr, p
jasypt++ jasypt++
} }
} }
encryption := "none: plain text"
if jasypt > 0 {
encryption = fmt.Sprintf("jasypt: %d of %d values", jasypt, len(keys))
}
return []provider.Attr{ return []provider.Attr{
{Label: "encryption", Value: encryption}, {Label: "encryption", Value: jasyptShare(jasypt, len(keys))},
{Label: "other keys", Value: fmt.Sprintf("%d, not matched by the secret patterns", len(keys)-secrets)}, {Label: "other keys", Value: fmt.Sprintf("%d, not matched by the secret patterns", len(keys)-secrets)},
}, plain }, plain
} }
+5 -4
View File
@@ -141,7 +141,7 @@ jasypt:
encryptor: encryptor:
password: ${JASYPT_ENCRYPTOR_PASSWORD} password: ${JASYPT_ENCRYPTOR_PASSWORD}
app: app:
api-key: ENC(abc==) api-key: ENC(5IQUKT4B7j38TnZJ+BlWqo/zSkKHTjl4XRJyaQEHgkCy4r00avLt4dWqFB74X0Zw)
`, `,
"src/main/resources/application-local.properties": "server.port=8081\nspring.datasource.password=hunter2\n", "src/main/resources/application-local.properties": "server.port=8081\nspring.datasource.password=hunter2\n",
"applicationset/applications.yaml": "kind: ApplicationSet\n", "applicationset/applications.yaml": "kind: ApplicationSet\n",
@@ -180,12 +180,13 @@ app:
keys, _ := p.Objects(ctx, yml, provider.TypeSecret) keys, _ := p.Objects(ctx, yml, provider.TypeSecret)
var names []string var names []string
for _, k := range keys { for _, k := range keys {
names = append(names, k.Name) names = append(names, strings.TrimSpace(k.Name+" "+k.Detail))
} }
if want := []string{"spring.datasource.password", "jasypt.encryptor.password", "app.api-key"}; !slices.Equal(names, want) { if want := []string{"spring.datasource.password reference", "jasypt.encryptor.password reference", "app.api-key jasypt"}; !slices.Equal(names, want) {
t.Errorf("keys %v, want %v", names, want) t.Errorf("keys %v, want %v", names, want)
} }
if v, err := p.Value(ctx, keys[2]); err != nil || v.Reveal() != "ENC(abc==)" { t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "test-password")
if v, err := p.Value(ctx, keys[2]); err != nil || v.Reveal() != "hunter2" {
t.Errorf("value: %q %v", v.Reveal(), err) t.Errorf("value: %q %v", v.Reveal(), err)
} }
link := provider.Object{Container: provider.Container{Source: srcs[0], Name: "src/main/resources/application.yml"}, Name: "spring.datasource.password"} link := provider.Object{Container: provider.Container{Source: srcs[0], Name: "src/main/resources/application.yml"}, Name: "spring.datasource.password"}
+3
View File
@@ -137,6 +137,9 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
line = sty.Muted.Render(o.Name + " (disabled)") line = sty.Muted.Render(o.Name + " (disabled)")
} }
line = marks(sty, n, o.Container.Source.Provider, m.recentIcon, m.now()) + line line = marks(sty, n, o.Container.Source.Provider, m.recentIcon, m.now()) + line
if o.Detail != "" {
line += sty.Muted.Render(" " + o.Detail)
}
switch state, left := m.expiry(o); state { switch state, left := m.expiry(o); state {
case expiry.Expired: case expiry.Expired:
line += " " + sty.Error.Render("✗ expired") line += " " + sty.Error.Render("✗ expired")
+15
View File
@@ -38,11 +38,13 @@ func (m *Model) storeEntries() (favs, recent []option) {
return nil return nil
} }
var out []entry var out []entry
detail := m.objectDetail(key)
add := func(path []string, acct string, depth int, run func(*Model) tea.Cmd) { add := func(path []string, acct string, depth int, run func(*Model) tea.Cmd) {
account := "" account := ""
if p, ok := m.byID[acct]; ok { if p, ok := m.byID[acct]; ok {
account = accountOf(provider.Source{Kind: p.Kind(), ProviderLabel: p.Label()}) account = accountOf(provider.Source{Kind: p.Kind(), ProviderLabel: p.Label()})
} }
account = strings.Trim(detail+" · "+account, " ·")
sort := strings.ToLower(strings.Join(path, "/")) + " " + account sort := strings.ToLower(strings.Join(path, "/")) + " " + account
out = append(out, entry{depth, sort, option{ out = append(out, entry{depth, sort, option{
label: m.pathLabel(r.Kind, path, account), note: n.Note, mark: " ", label: m.pathLabel(r.Kind, path, account), note: n.Note, mark: " ",
@@ -157,6 +159,19 @@ func (m Model) inPlainEnv(r ref.Ref) bool {
return false return false
} }
// objectDetail is the Detail of the secret with the store key key, as far
// as the lists loaded tell.
func (m Model) objectDetail(key string) string {
for _, e := range m.cache.objects {
for _, o := range e.value {
if o.Detail != "" && objectKey(o) == key {
return o.Detail
}
}
}
return ""
}
// repoLabel draws a repo, or a file in it, as everywhere else: the repo // repoLabel draws a repo, or a file in it, as everywhere else: the repo
// icon and colour, "app / apps/api/.env". // icon and colour, "app / apps/api/.env".
func (m Model) repoLabel(repo string, file ...string) string { func (m Model) repoLabel(repo string, file ...string) string {
+1 -1
View File
@@ -90,7 +90,7 @@ func (m *Model) searchEntries() (sources, items []option) {
if err != nil { if err != nil {
continue continue
} }
suffix := accountOf(src) suffix := strings.Trim(o.Detail+" · "+accountOf(src), " ·")
if t != provider.TypeSecret { if t != provider.TypeSecret {
typ := strings.TrimSuffix(strings.ToLower(pluralLabels[t]), "s") // "key" typ := strings.TrimSuffix(strings.ToLower(pluralLabels[t]), "s") // "key"
suffix = strings.Trim(typ+" · "+suffix, " ·") suffix = strings.Trim(typ+" · "+suffix, " ·")