diff --git a/internal/share/share.go b/internal/share/share.go index 172235e..7830f24 100644 --- a/internal/share/share.go +++ b/internal/share/share.go @@ -203,7 +203,7 @@ func (s Sealed) Open(key []byte) ([]File, error) { if err != nil { var wrong *age.NoIdentityMatchError if errors.As(err, &wrong) { - return nil, errors.New("the key doesn't decrypt it: the link may have been made with another version of " + s.Key.Name) + return nil, s.wrongKey() } return nil, fmt.Errorf("can't decrypt the link: %w", err) } @@ -228,6 +228,21 @@ func (s Sealed) Open(key []byte) ([]File, error) { return out, nil } +// wrongKey says why the key, read fine, doesn't decrypt the link: its +// value isn't the one the link was made with. +func (s Sealed) wrongKey() error { + name := s.Key.Name + switch { + case s.Key.Kind == provider.KindAzure && s.Key.Version != "": + // A version's value never changes. + return fmt.Errorf("%s at version %s doesn't decrypt it: the link may be damaged", name, s.Key.Version) + case s.Key.Kind == provider.KindAzure: + return fmt.Errorf("%s's current version isn't the one the link was made with, so it can't decrypt it: ask for a new link", name) + } + // Databricks secrets and SOPS values are changed in place. + return fmt.Errorf("%s has changed since the link was made, so it can't decrypt it: ask for a new link", name) +} + // CheckPath accepts a path relative to a repo's root, with slashes, of an // env file (.env, .env.local, prod.env) outside .git: what a link may // write. diff --git a/internal/share/share_test.go b/internal/share/share_test.go index d61f803..069c68f 100644 --- a/internal/share/share_test.go +++ b/internal/share/share_test.go @@ -49,11 +49,33 @@ func TestSealOpen(t *testing.T) { if len(got) != 2 || got[1].Path != files[1].Path || string(got[1].Data) != string(files[1].Data) { t.Errorf("opened %+v", got) } - if _, err := s.Open([]byte("another key, long enough to use")); err == nil || !strings.Contains(err.Error(), "doesn't decrypt") { + if _, err := s.Open([]byte("another key, long enough to use")); err == nil || !strings.Contains(err.Error(), "env-share at version 91d3e0 doesn't decrypt it") { t.Errorf("wrong key: %v", err) } } +func TestWrongKeySaysWhy(t *testing.T) { + files := []File{{Path: ".env", Data: []byte("A=1\n")}} + for link, want := range map[string]string{ + "lazysecrets://databricks/adb-1.1.azuredatabricks.net/team/env-share": "env-share has changed since the link was made", + "lazysecrets://repo/git.example.com%2Fteam%2Fapp/secrets.yaml/share": "share has changed since the link was made", + "lazysecrets://azure/kv-team.vault.azure.net/secrets/env-share": "env-share's current version isn't the one the link was made with", + } { + k, err := ParseKey(link) + if err != nil { + t.Fatal(err) + } + r, err := Seal(Header{Key: k}, pass, files, Rules{}) + if err != nil { + t.Fatal(err) + } + s, _ := Read(r) + if _, err := s.Open([]byte("rotated since, and long enough to be a key")); err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("%s: %v, want %q", link, err, want) + } + } +} + func TestSealRefuses(t *testing.T) { ok := []File{{Path: ".env", Data: []byte("A=1")}} for name, c := range map[string]struct { diff --git a/internal/ui/app/sharefiles.go b/internal/ui/app/sharefiles.go index 940a006..e5cf8a5 100644 --- a/internal/ui/app/sharefiles.go +++ b/internal/ui/app/sharefiles.go @@ -304,16 +304,16 @@ func (m *Model) handleShareOpened(msg shareOpenedMsg) tea.Cmd { } // openFailed says why a share link couldn't be decrypted, in a popup -// where its offer was: its first line, or with whole all of it +// where its offer was: its first line (wrapped), or with whole all of it // (Key Vault's refusals say what to check on later lines). From there, // the key's secret can be gone to: its version, which may be disabled. func (m *Model) openFailed(s share.Sealed, err error, whole bool) { width := max(30, min(m.w-12, 76)) text := capitalize(err.Error()) first, rest, _ := strings.Cut(text, "\n") - cut := !whole && (rest != "" || ansi.StringWidth(first) > width-2) + cut := !whole && rest != "" // more than the reason: what to check if !whole { - text = ansi.Truncate(first, width-2, "…") + text = first } var lines []string for i, l := range strings.Split(ansi.Wordwrap(text, width-2, " "), "\n") {