feat: a key that no longer decrypts a share link says why
By where it lives: a Databricks secret or SOPS value has changed since the link was made, an Azure secret's current version isn't the link's (unpinned), and a pinned version can't have changed, so the link may be damaged. The popup wraps the reason instead of cutting it; r is for what Key Vault adds to check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
3 files changed
+42
-5
No files matched your search
+16
-1
@@ -203,7 +203,7 @@ func (s Sealed) Open(key []byte) ([]File, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
var wrong *age.NoIdentityMatchError
|
var wrong *age.NoIdentityMatchError
|
||||||
if errors.As(err, &wrong) {
|
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)
|
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
|
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
|
// 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
|
// env file (.env, .env.local, prod.env) outside .git: what a link may
|
||||||
// write.
|
// write.
|
||||||
|
|||||||
@@ -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) {
|
if len(got) != 2 || got[1].Path != files[1].Path || string(got[1].Data) != string(files[1].Data) {
|
||||||
t.Errorf("opened %+v", got)
|
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)
|
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) {
|
func TestSealRefuses(t *testing.T) {
|
||||||
ok := []File{{Path: ".env", Data: []byte("A=1")}}
|
ok := []File{{Path: ".env", Data: []byte("A=1")}}
|
||||||
for name, c := range map[string]struct {
|
for name, c := range map[string]struct {
|
||||||
|
|||||||
@@ -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
|
// 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,
|
// (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.
|
// 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) {
|
func (m *Model) openFailed(s share.Sealed, err error, whole bool) {
|
||||||
width := max(30, min(m.w-12, 76))
|
width := max(30, min(m.w-12, 76))
|
||||||
text := capitalize(err.Error())
|
text := capitalize(err.Error())
|
||||||
first, rest, _ := strings.Cut(text, "\n")
|
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 {
|
if !whole {
|
||||||
text = ansi.Truncate(first, width-2, "…")
|
text = first
|
||||||
}
|
}
|
||||||
var lines []string
|
var lines []string
|
||||||
for i, l := range strings.Split(ansi.Wordwrap(text, width-2, " "), "\n") {
|
for i, l := range strings.Split(ansi.Wordwrap(text, width-2, " "), "\n") {
|
||||||
|
|||||||
Reference in new issue
Block a user