Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions functional-tests/res/format-2.enc.ini
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions functional-tests/res/format.enc.env
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions functional-tests/res/format.enc.ini
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions functional-tests/res/format.enc.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
22 changes: 22 additions & 0 deletions functional-tests/res/format.enc.yaml
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions functional-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}
6 changes: 4 additions & 2 deletions stores/ini/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions stores/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down