fix: decrypt SOPS .env keys written with export or spaces (#78)
Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
This commit was merged in pull request #78.
This commit is contained in:
1 parent
637bf6a253
commit
39cbac9aab
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")
|
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)
|
||||||
extract = extractFor(file, o.Name)
|
|
||||||
}
|
}
|
||||||
if _, isSops := readMeta(file); !isSops {
|
if _, isSops := readMeta(file); !isSops {
|
||||||
return plainValue(file, o.Name)
|
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 := exec.CommandContext(ctx, p.s.Sops, "decrypt", "--extract", extract, file)
|
||||||
cmd.Dir = filepath.Dir(file) // sops looks for .sops.yaml from here
|
cmd.Dir = filepath.Dir(file) // sops looks for .sops.yaml from here
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -1045,7 +1050,8 @@ func parseKeys(file string, b []byte) ([]key, error) {
|
|||||||
if !ok || strings.HasPrefix(k, "sops_") {
|
if !ok || strings.HasPrefix(k, "sops_") {
|
||||||
continue
|
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
|
return out, nil
|
||||||
case "ini":
|
case "ini":
|
||||||
@@ -1110,48 +1116,23 @@ func escape(k string) string {
|
|||||||
return strings.NewReplacer(`\`, `\\`, `.`, `\.`, `[`, `\[`, `]`, `\]`).Replace(k)
|
return strings.NewReplacer(`\`, `\\`, `.`, `\.`, `[`, `\[`, `]`, `\]`).Replace(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractFor rebuilds the extract path from a key name, for objects made
|
// extractOf is the sops --extract path of the key named name, for objects
|
||||||
// from a link (db.password, list[0], app\.name). A dotenv file is flat:
|
// made from a link.
|
||||||
// its names are the keys as they are.
|
func extractOf(file, name string) (string, error) {
|
||||||
func extractFor(file, name string) string {
|
b, err := os.ReadFile(file)
|
||||||
if formatOf(file) == "dotenv" {
|
if err != nil {
|
||||||
return quote(name)
|
return "", err
|
||||||
}
|
}
|
||||||
var b, part strings.Builder
|
keys, err := parseKeys(file, b)
|
||||||
flush := func() {
|
if err != nil {
|
||||||
if part.Len() > 0 {
|
return "", err
|
||||||
b.WriteString(quote(part.String()))
|
}
|
||||||
part.Reset()
|
for _, k := range keys {
|
||||||
|
if k.name == name {
|
||||||
|
return k.extract, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := 0; i < len(name); i++ {
|
return "", fmt.Errorf("%s isn't in %s", name, filepath.Base(file))
|
||||||
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") == ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func absPath(p string) string {
|
func absPath(p string) string {
|
||||||
|
|||||||
@@ -237,6 +237,25 @@ func TestDecrypt(t *testing.T) {
|
|||||||
t.Errorf("from a link: %q %v", v.Reveal(), err)
|
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.
|
// Without the key: sops' own error.
|
||||||
t.Setenv("SOPS_AGE_KEY_FILE", filepath.Join(dir, "missing.txt"))
|
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") {
|
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
|
var got []string
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
got = append(got, k.name+" "+k.extract)
|
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]"]`
|
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 {
|
if strings.Join(got, "|") != want {
|
||||||
t.Errorf("keys = %s\nwant %s", 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