fix: decrypt SOPS .env keys written with export or spaces
sops keeps everything before the = in a dotenv key, so `export TOKEN=…` is the key `export TOKEN`. lazysecrets asked sops for `TOKEN` and the value couldn't be decrypted, from the list or a link. - The sops path is the key as written; names and links are unchanged. - A link finds its key's path in the file, as the list does, instead of rebuilding it from the name.
This commit is contained in:
2 files changed
+53
-46
No files matched your search
@@ -392,11 +392,16 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
||||
file, extract, ok := strings.Cut(o.ID, "\x00")
|
||||
if !ok { // made from a link: the repo's root, the file's path, the key
|
||||
file = filePath(o.Container)
|
||||
extract = extractFor(file, o.Name)
|
||||
}
|
||||
if _, isSops := readMeta(file); !isSops {
|
||||
return plainValue(file, o.Name)
|
||||
}
|
||||
if !ok {
|
||||
var err error
|
||||
if extract, err = extractOf(file, o.Name); err != nil {
|
||||
return provider.SecretValue{}, err
|
||||
}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, p.s.Sops, "decrypt", "--extract", extract, file)
|
||||
cmd.Dir = filepath.Dir(file) // sops looks for .sops.yaml from here
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -1045,7 +1050,8 @@ func parseKeys(file string, b []byte) ([]key, error) {
|
||||
if !ok || strings.HasPrefix(k, "sops_") {
|
||||
continue
|
||||
}
|
||||
out = append(out, key{name: k, extract: quote(k), encrypted: strings.HasPrefix(v, "ENC["), line: i + 1})
|
||||
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})
|
||||
}
|
||||
return out, nil
|
||||
case "ini":
|
||||
@@ -1110,48 +1116,23 @@ func escape(k string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `.`, `\.`, `[`, `\[`, `]`, `\]`).Replace(k)
|
||||
}
|
||||
|
||||
// extractFor rebuilds the extract path from a key name, for objects made
|
||||
// from a link (db.password, list[0], app\.name). A dotenv file is flat:
|
||||
// its names are the keys as they are.
|
||||
func extractFor(file, name string) string {
|
||||
if formatOf(file) == "dotenv" {
|
||||
return quote(name)
|
||||
// extractOf is the sops --extract path of the key named name, for objects
|
||||
// made from a link.
|
||||
func extractOf(file, name string) (string, error) {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var b, part strings.Builder
|
||||
flush := func() {
|
||||
if part.Len() > 0 {
|
||||
b.WriteString(quote(part.String()))
|
||||
part.Reset()
|
||||
keys, err := parseKeys(file, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, k := range keys {
|
||||
if k.name == name {
|
||||
return k.extract, nil
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(name); i++ {
|
||||
switch c := name[i]; c {
|
||||
case '\\':
|
||||
if i+1 < len(name) {
|
||||
i++
|
||||
}
|
||||
part.WriteByte(name[i])
|
||||
case '.':
|
||||
flush()
|
||||
case '[':
|
||||
end := strings.IndexByte(name[i:], ']')
|
||||
if end < 0 || !digits(name[i+1:i+end]) {
|
||||
part.WriteByte(c)
|
||||
continue
|
||||
}
|
||||
flush()
|
||||
b.WriteString(name[i : i+end+1])
|
||||
i += end
|
||||
default:
|
||||
part.WriteByte(c)
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func digits(s string) bool {
|
||||
return s != "" && strings.Trim(s, "0123456789") == ""
|
||||
return "", fmt.Errorf("%s isn't in %s", name, filepath.Base(file))
|
||||
}
|
||||
|
||||
func absPath(p string) string {
|
||||
|
||||
@@ -237,6 +237,25 @@ func TestDecrypt(t *testing.T) {
|
||||
t.Errorf("from a link: %q %v", v.Reveal(), err)
|
||||
}
|
||||
|
||||
// A dotenv file's export lines: sops keeps export in the key.
|
||||
os.WriteFile(plain, []byte("export TOKEN=s3cret\n"), 0o600)
|
||||
enc, err = exec.Command("sops", "encrypt", "--age", recipient, "--input-type", "dotenv", "--output-type", "dotenv", plain).Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
envFile := filepath.Join(root, "prod.env")
|
||||
os.WriteFile(envFile, enc, 0o600)
|
||||
envC := provider.Container{Source: srcs[0], ID: envFile, Name: "prod.env"}
|
||||
envKeys, _ := p.Objects(ctx, envC, provider.TypeSecret)
|
||||
if len(envKeys) != 1 {
|
||||
t.Fatalf("env keys: %v", envKeys)
|
||||
}
|
||||
for _, o := range []provider.Object{envKeys[0], {Container: provider.Container{Source: srcs[0], Name: "prod.env"}, Name: "TOKEN"}} {
|
||||
if v, err := p.Value(ctx, o); err != nil || v.Reveal() != "s3cret" {
|
||||
t.Errorf("export line (link: %t): %q %v", o.ID == "", v.Reveal(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Without the key: sops' own error.
|
||||
t.Setenv("SOPS_AGE_KEY_FILE", filepath.Join(dir, "missing.txt"))
|
||||
if _, err := p.Value(ctx, keys[0]); err == nil || !strings.Contains(err.Error(), "no identity matched any of the recipients") {
|
||||
@@ -554,15 +573,22 @@ func TestKeyNamesEscaped(t *testing.T) {
|
||||
var got []string
|
||||
for _, k := range keys {
|
||||
got = append(got, k.name+" "+k.extract)
|
||||
if e := extractFor("s.yaml", k.name); e != k.extract {
|
||||
t.Errorf("extractFor(%q) = %s, want %s", k.name, e, k.extract)
|
||||
}
|
||||
}
|
||||
want := `app\.name ["app.name"]|db.password ["db"]["password"]|list[0] ["list"][0]|list[1] ["list"][1]|odd\[1\] ["odd[1]"]`
|
||||
if strings.Join(got, "|") != want {
|
||||
t.Errorf("keys = %s\nwant %s", strings.Join(got, "|"), want)
|
||||
}
|
||||
if e := extractFor(".env", "spring.url"); e != `["spring.url"]` {
|
||||
t.Errorf("dotenv: %s", e)
|
||||
|
||||
keys, err = parseKeys(".env", []byte("spring.url=a\nexport TOKEN=b\nSPACED = c\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = nil
|
||||
for _, k := range keys {
|
||||
got = append(got, k.name+" "+k.extract)
|
||||
}
|
||||
want = `spring.url ["spring.url"]|TOKEN ["export TOKEN"]|SPACED ["SPACED "]`
|
||||
if strings.Join(got, "|") != want {
|
||||
t.Errorf("dotenv keys = %s\nwant %s", strings.Join(got, "|"), want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user