diff --git a/functional-tests/res/format-2.enc.ini b/functional-tests/res/format-2.enc.ini new file mode 100644 index 0000000000..1ad1cd93ea --- /dev/null +++ b/functional-tests/res/format-2.enc.ini @@ -0,0 +1,22 @@ +[foo] +bar = ENC[AES256_GCM,data:sYUq,iv:IFRjAFMJoylEg0HWkxWi118rZhHDX0NCWW9V2UcnyjI=,tag:vZ1xH/wvEEBUqItnwNYaFw==,type:str] + +[sops] +lastmodified = 2026-05-22T20:15:23Z +mac = ENC[AES256_GCM,data:h2dz/371T6O0T9KOpaD5xGS4naYYXQPkTSfNSWKwlGhUGPNUCJbVAlDAuGWensdKE4k3B17Vm76zRQ6UdNxzTP++heSU+4ImF2qfdjrTvNPyr9Hl9BTVrVN8PD/XzUzH7FGRRBAlU4A0Zim4+Ktaz6s8Kse9lDtv9LWW3WZULYI=,iv:lDrw5xKxV4kDSKpJWf0z+XWaHt8An70xt2OZZhueUzg=,tag:vstWUHF5KIMpA8EM9B5kyg==,type:str] +pgp__list_0__map_created_at = 2026-05-22T20:15:17Z +pgp__list_0__map_enc = """-----BEGIN PGP MESSAGE----- + +wcBMAyUpShfNkFB/AQf/eLAAHlnmaYZGuitpb7FU3tarxgRLHKoRSYCXBWg1CSPJ +ySY5mP0oE6BPeCa14NMafKpwd4XQ9IHpNxSVJKRsKkY0EI67rFT85CXgIApkxNRD +6FgvMaJD1ueJt9o4jLFGkFM0IyfNY4eV05rBS7EBLdIK+9n0CoBJxz/b+24pU+NW +eJA7doNRWNRjUl9SteUgx85+h37uDZmS4R/5I0y5DEvLvLlgZiriu7l73f3M6GeD +C94FbpBg6SHFqfsqOjrBfBSkulAvBIsgwnEEzRA7imJyBCPSM7raZSjhqUz5AFTP +gyRbcD5C4v/ET7cLsy4TWRUL9ei/8SVB0fkPdlD+MdJRAWgq5y+gy2p/wDTGN3W3 +uOI80TLunC+iYNayPqCSCxs24lCRw1VN4/7Vy/SSAMpKuFMEKBUrlPGV4cgIkqI+ +1bfP6Q44c+7Ut5wiMJfjdm5U +=j3Rn +-----END PGP MESSAGE-----""" +pgp__list_0__map_fp = FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 +unencrypted_suffix = _unencrypted +version = 3.13.1 diff --git a/functional-tests/res/format.enc.env b/functional-tests/res/format.enc.env new file mode 100644 index 0000000000..5dbc92a4d7 --- /dev/null +++ b/functional-tests/res/format.enc.env @@ -0,0 +1,8 @@ +foo =ENC[AES256_GCM,data:XBV7lQ==,iv:d1FvB2AftTKfYFDO2XspPuv5IKcRhrljmRNCtbWoj7Q=,tag:pprVAdWf3q4Bb4oRPXZlBQ==,type:str] +sops_lastmodified=2026-05-22T20:15:29Z +sops_mac=ENC[AES256_GCM,data:evEAiDYeDG8/wtRoQbbpbjsGYvSct7SGOsFVKnph18e+6xQ9xqV6IwAtkxlfAizUpnkJSYvzebj5ZwI93DEl+lr8gA+JKTxw7YtEr9NejTz6lXaJtjKUZ9dmzqWkUoI/rexbaMncet9SLeKldrpGwA2DNdwjBYGj5FI8nRuN6DU=,iv:fQ864RUZhmoDzyLaYV1ET3faFHnlOszNAtddT1/nAoo=,tag:c98HSPfDpN7abMb66clDWw==,type:str] +sops_pgp__list_0__map_created_at=2026-05-22T20:15:26Z +sops_pgp__list_0__map_enc=-----BEGIN PGP MESSAGE-----\n\nwcBMAyUpShfNkFB/AQf+M//bb390sj/AhiuF4jDNz5olNzB1+Ha2j7hpCpwmsDFG\nhO9H7RQ+LJMwPTXPFI6/BDODAGg1/unEt8JNv+NGEInzLpyRhdY4SMBRhch+Drnr\n/9CK1DgEL+M66S2pydfFs0ZbI8XFRgC2nxt720e5FFlHwUj/LsVyI6zcrb5GsG5Y\n4TW4Tv8Cod0Mqh0ytqXGCkjDUqj2herl/DNELDSaKQKzlqdkWtOBH9vGLk/bzeHk\nfa6HOylQLX0sZfkbgHb9Yo14DLNJJoNUC/COWdwFGdWDebRChPcVwUmRe07YG3+5\nU42SfhukBg2Qjl7sgPuBsQSowSOk3xw5dxCap2emP9JRAbkA0A16YvpTJ2T2ufQz\noKKANmJ2EZDeZiekm/onGQ48tRcTgA5CuPg9d2766VfR9htoBa8x3HzmbBMzZN4S\nZ1SkIvqPKtBo2g5JslnH5ApZ\n=5coz\n-----END PGP MESSAGE----- +sops_pgp__list_0__map_fp=FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 +sops_unencrypted_suffix=_unencrypted +sops_version=3.13.1 diff --git a/functional-tests/res/format.enc.ini b/functional-tests/res/format.enc.ini new file mode 100644 index 0000000000..f43208768c --- /dev/null +++ b/functional-tests/res/format.enc.ini @@ -0,0 +1,11 @@ +[foo] +bar = ENC[AES256_GCM,data:Upwq,iv:PjRmKlK3MuUzTjIwr6HX2t5kMXkTFwJw2cdL9/zXQYk=,tag:LASj5iJpy+8UJshqSV3ICg==,type:str] + +[sops] +lastmodified = 2026-05-22T20:15:58Z +mac = ENC[AES256_GCM,data:sdSDymN65DegskpmhkPMwow83N/k3wspd7oYMMS5dqG2N2ekWkZj9KWQ1W6F5zVFa8oqcjiscJMIZNFIFvcl1UEwhtSTs/Lzkmt8vx2cSgrf0Dr6zuqLHV2ilSDhTZ0cKbf5YzKzGa4H/X/09jLskRBCKIX42YKlSMV6WCFYfDU=,iv:5h0uMZ7mJLXepf9QtT9QSKmdt15TCcawy7cAt4k5rq0=,tag:iQO1nKpJDvpw5vpT44HF4A==,type:str] +pgp__list_0__map_created_at = 2026-05-22T20:15:53Z +pgp__list_0__map_enc = -----BEGIN PGP MESSAGE-----\n\nwcBMAyUpShfNkFB/AQf/daFIlZp0nciQLo/KV21fWPifgq9fyTIZN+ROZGU9HUOk\nXCnNaM14e7u1jfpvH+7sHytvxX63U7q+8FPujhcHI2thJ5o7iZRykD6f//cAC74d\nA3FGEhVl0lZsTiPd4m5jI7e5qaQLpUyh6rxx38RTyjPX7OC+hOLVZuLVS7eo39DH\nrfiP02K0Uj+riceLZ9WLZhgPNdxCUXrTJt24IS4frEh5WKnlkBx+4pqzG+Z5xXpu\ne1xuyPr/8jQTXQD4zmT4GjOowd9H46wKV/a23+FJl9KCSowPiIeuQ5ZNYg16R0oj\nxuU7qtdUZqgXNw/iHxub9crG7tdI9r5H9nmGLH2Jq9JRAbo2TdE9b+rTLo6FJG0f\n2W5rImXsH8THsrw6KyPtrkr3rdECDl0LFCaUm1I2gFev8dvXhdjhymUW0f1Cy+1w\nNqNfxIqit8lChU9/N6wnAqLM\n=Axgz\n-----END PGP MESSAGE----- +pgp__list_0__map_fp = FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 +unencrypted_suffix = _unencrypted +version = 3.13.2 diff --git a/functional-tests/res/format.enc.json b/functional-tests/res/format.enc.json new file mode 100644 index 0000000000..35daddf6f3 --- /dev/null +++ b/functional-tests/res/format.enc.json @@ -0,0 +1,16 @@ +{ + "foo": "ENC[AES256_GCM,data:fvD/,iv:k3Mzfy+W8e3JGCaehDIOwoJDFGOWGoV+peBYbWdmb70=,tag:FeziVN+GUFROjEiQl+RsSw==,type:str]", + "sops": { + "lastmodified": "2026-05-22T20:16:18Z", + "mac": "ENC[AES256_GCM,data:5ISldk/tHEMc4/m4e+O5jSCxLIcJnzMzkKOTHcu9HfB+xRtEBX34CsIHByyFkRXNHkQQtcZwCDgBrqaTvbqD47vSVoqhcoV5PMSvShFaWUPLy2d1NdH+43+JBTPGVuae6No6H0xj1Wf5gE+Rj/Ql/g6o8EIz+uoPlBSmfyqHvWA=,iv:NsFPBoAsI1Nog0781MtAlPWFs7kveSvE+JMGUdekMRA=,tag:NX+MlNcRnCGPAq58ewLfHQ==,type:str]", + "pgp": [ + { + "created_at": "2026-05-22T20:16:08Z", + "enc": "-----BEGIN PGP MESSAGE-----\n\nwcBMAyUpShfNkFB/AQf9GwCZN4BQFsgIx/yneSe78+kmOtXc84n/5yW2emuEKki7\n9JdMaJwgqWZ1R6WjthP9shc52ZIh3n0Q+NPrvqMcf4NFw7fA4TOeZ+Jmtg098JYc\nKDbmUqDsLEWJk2m1QIU6DnGaAV1dYiMB0beDQtJjXl0y3Qh5m2oqAvnGt8W/IsRR\nRhN5qk7iRsOgf/+r4d8DD/fo7IchL+HRcSFSqZC79zgxu+t1eK+p2zfSccl0FUYr\nNi+L4CVV/qi2BaBAxAbET40Xnntrv123gDDg/oWT6iIWXiQfoLkakf+FJgtx2G++\nEKA2ETj23XLiSLnaSA5tMXunA9W448phJaLf9cr4OtJRAbsdhc/ctu8HpRQ2s5jr\ny3hmEDUtBivHCA1ar/0mMNA6bj7VfZzTzZnNyoDrBpxP01qTpcaTyFNGaWDdkFNj\naDkFu537VMGeWf2t0Jev+o69\n=7UUe\n-----END PGP MESSAGE-----", + "fp": "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4" + } + ], + "unencrypted_suffix": "_unencrypted", + "version": "3.13.1" + } +} diff --git a/functional-tests/res/format.enc.yaml b/functional-tests/res/format.enc.yaml new file mode 100644 index 0000000000..cdd6788ea7 --- /dev/null +++ b/functional-tests/res/format.enc.yaml @@ -0,0 +1,22 @@ +foo: ENC[AES256_GCM,data:8CvE,iv:tjchf/lmMpXDOA1tLE/N4oY9IqJIzfQ/7NlvQlDUtoA=,tag:1aFCHIRp3j/VPNcCEsE95A==,type:str] +sops: + lastmodified: "2026-05-22T20:16:23Z" + mac: ENC[AES256_GCM,data:t5sIFo7psXtfOiPdp9psT3nmgO/sH2GojpOIluWadfDJGPIsucbPrj906LwpVqva/4bq7q1aUKhm7X4RwfkvVsdgRbKr6azww5uvTUFTN58Vi0jxyhSaUh96+aVK02SkPbzx5oSEK0R9M/ZWSKnP8VY6AKTlP6I3I2P8HAEK6xs=,iv:mhrVlCOWU0MnIYxnccGGFDCPAXsNwl9sopIm7f74kT8=,tag:w+XIuL4lhKHlgGOlclR2PA==,type:str] + pgp: + - created_at: "2026-05-22T20:16:20Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + wcBMAyUpShfNkFB/AQf/b1Bdb5X/56d4Oe/5ZoEvKmjN68fr5n9c7gK8qW8Y+Q/L + vbfY+l/3HDta5fHHe1RIvyHle/Zgrh1PcQELvhPwsp2nW3NSPsCOCvemziNv7b2b + c57DjDyt97C2UDG82jBCwMUYp5ekYuXleTcMLTJF2V9GQ5jeGDflpfrrPXoI22bh + LJBAGtQp0kQQgoQSdLEQ9fCVAt0UV4MfZ28I9ezUoihrVydLFGtYFqqQCyLdpDfW + jXQk2QHM+FUPWJyaCZlFJYv29LYz9AYG0db6FLcUUrVbkujdPZak107va8r6FDFF + xwJBfau2I/ocQwkWv99+/+AYCh7+DWkwJ20W8uFBytJRAeL2P3JHSof0YsnpeRDH + MJzf3oxf6c61w3mCxA6p/sgeUpYU0Ja/MNeRTD4c5xt6AppbQfNkyeI/BcvSD3UQ + FTBiiWDU11xm4NWm0sIRbnof + =5PzX + -----END PGP MESSAGE----- + fp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index 6530d16bd1..d5c010aacb 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -1650,4 +1650,92 @@ bar: |- "filename did not end with 'foobar'" ); } + + #[test] + fn decrypt_format() { + // YAML + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("res/format.enc.yaml") + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "SOPS didn't return successfully"); + assert!( + String::from_utf8_lossy(&output.stdout) == "foo: bar\n", + "Unexpected decrypted content" + ); + + // JSON + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("res/format.enc.json") + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "SOPS didn't return successfully"); + assert!( + String::from_utf8_lossy(&output.stdout) == "{\n\t\"foo\": \"bar\"\n}\n", + "Unexpected decrypted content" + ); + + // DotEnv + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("res/format.enc.env") + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "SOPS didn't return successfully"); + assert!( + String::from_utf8_lossy(&output.stdout) == "foo = bar\n", + "Unexpected decrypted content" + ); + + // INI + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("res/format.enc.ini") + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "SOPS didn't return successfully"); + assert!( + String::from_utf8_lossy(&output.stdout) == "[foo]\nbar = baz\n", + "Unexpected decrypted content" + ); + + // INI (SOPS 3.13.0 / 3.13.1) + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("res/format-2.enc.ini") + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "SOPS didn't return successfully"); + assert!( + String::from_utf8_lossy(&output.stdout) == "[foo]\nbar = baz\n", + "Unexpected decrypted content" + ); + } } diff --git a/stores/ini/store.go b/stores/ini/store.go index 8d49dc5ec3..9378fc62de 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -138,7 +138,8 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { return sops.Tree{}, err } branches, metadata, err := stores.ExtractMetadata(branches, stores.MetadataOpts{ - Flatten: stores.MetadataFlattenBelowTop, + Flatten: stores.MetadataFlattenBelowTop, + EscapeNewlines: true, }) if err != nil { return sops.Tree{}, err @@ -162,7 +163,8 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { // runtime object func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { branches, err := stores.SerializeMetadata(in, stores.MetadataOpts{ - Flatten: stores.MetadataFlattenBelowTop, + Flatten: stores.MetadataFlattenBelowTop, + EscapeNewlines: true, }) if err != nil { return nil, fmt.Errorf("Error marshaling metadata: %s", err) diff --git a/stores/metadata.go b/stores/metadata.go index d32c619cb9..475786175c 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -28,6 +28,12 @@ const ( type MetadataOpts struct { Flatten MetadataFlatten + // Whether strings need "\n" replaced by "\\n". + // Only used if Flatten is not MetadataFlattenNone. + // This does provide a double escape for newlines, since the store itself + // is already expected to take care of them. This is mainly needed for + // backwards compatibility with the INI store. + EscapeNewlines bool } // SopsPrefix is the prefix for all metadata entry keys. @@ -145,6 +151,16 @@ func ExtractMetadata(branches sops.TreeBranches, opts MetadataOpts) (sops.TreeBr } if opts.Flatten != MetadataFlattenNone { var err error + if opts.EscapeNewlines { + for i, item := range metadataTree { + if value, ok := item.Value.(string); ok { + metadataTree[i] = sops.TreeItem{ + Key: item.Key, + Value: strings.ReplaceAll(value, "\\n", "\n"), + } + } + } + } metadataTree, err = unflattenTreeBranch(metadataTree) if err != nil { return nil, sops.Metadata{}, err @@ -259,6 +275,16 @@ func SerializeMetadata(data sops.Tree, opts MetadataOpts) (sops.TreeBranches, er if err != nil { return nil, fmt.Errorf("Error while flattening metadata: %w", err) } + if opts.EscapeNewlines { + for i, item := range md { + if value, ok := item.Value.(string); ok { + md[i] = sops.TreeItem{ + Key: item.Key, + Value: strings.ReplaceAll(value, "\n", "\\n"), + } + } + } + } } if opts.Flatten != MetadataFlattenFull { md = sops.TreeBranch{