Skip to content

v3.13.0 regression: can't decrypt INI files written by earlier versions; DecodeNewLines was dropped in #2120 #2188

@ojsef39

Description

@ojsef39

Summary

sops 3.13.0 cannot decrypt INI files that were encrypted with earlier versions (tested with files written by 3.10.2). Decryption fails with:

failed to create reader for decrypting sops data key with age:
failed to read header: parsing age header: failed to read intro:
invalid armor: invalid first line: "-----BEGIN AGE ENCRYPTED FILE

The same files decrypt successfully with sops 3.12.x. YAML/JSON/.env files written by the same older version still decrypt fine on 3.13.0; the regression is INI-specific.

Root cause

Looks like a side-effect of #2120 ("Use mapstructure to (de-)serialize internal metadata", merged 2026-05-08).

In stores/ini/store.go at v3.10.2, LoadEncryptedFile went through iniSectionToMetadata, which explicitly unescaped \n sequences before handing the armored age/PGP blob to the key service:

// v3.10.2 - stores/ini/store.go
func (store *Store) iniSectionToMetadata(sopsSection *ini.Section) (stores.Metadata, error) {
    metadataHash := make(map[string]interface{})
    for k, v := range sopsSection.KeysHash() {
        metadataHash[k] = v
    }
    stores.DecodeNewLines(metadataHash)        // <-- the relevant step
    err := stores.DecodeNonStrings(metadataHash)
    ...
    return stores.UnflattenMetadata(metadataHash)
}

In v3.13.0 this entire path is gone. LoadEncryptedFile now delegates to the generic stores.ExtractMetadata with MetadataFlattenBelowTop, which calls treeBranchToMetadata → mapstructure on the raw values returned by gopkg.in/ini.v1. gopkg.in/ini.v1 doesn't unescape \n by default, and mapstructure doesn't either, so the armored blob arrives at the age library as a single line:

-----BEGIN AGE ENCRYPTED FILE-----\nYWdl...\n-----END AGE ENCRYPTED FILE-----\n

(literal backslash-n characters). Age then fails on the armor header.

Reproduction

  1. With sops 3.10.2 (or any pre-3.13.0), age-encrypt an .ini file:
    [section]
    FOO = bar
  2. Upgrade to sops 3.13.0.
  3. sops --decrypt file.ini → "invalid armor: invalid first line" error above.
  4. Downgrade to 3.12.x → decrypts fine.

The relevant metadata line in such a file looks like:

age__list_0__map_enc = -----BEGIN AGE ENCRYPTED FILE-----\nYWdl...\n-----END AGE ENCRYPTED FILE-----\n

Questions for maintainers

  1. Is it expected that v3.13.0 INI emits the same \n-escaped armor format? If so, restoring an equivalent of DecodeNewLines in the new ExtractMetadata path (probably gated on the INI/.env stores) seems like the minimal fix.
  2. If the emit side now writes multi-line values differently, then INI files round-trip on 3.13.0 but old files are stranded; in that case a one-shot migration command or a documented "decrypt with 3.12, re-encrypt with 3.13" upgrade path would be helpful.
  3. Does the same regression affect dotenv files with armored payloads? (I didn't test.)

Environment

  • sops 3.13.0 (Nix, macOS arm64)
  • File originally written by sops 3.10.2
  • Encryption: age (single recipient)

Workaround for now: pin to sops 3.12.x.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions