Skip to content
Merged
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
11 changes: 11 additions & 0 deletions aes/cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"regexp"
"strconv"
"time"

"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/logging"
Expand Down Expand Up @@ -110,6 +111,10 @@ func (c Cipher) Decrypt(ciphertext string, key []byte, additionalData string) (p
plaintext = decryptedBytes
case "bool":
plaintext, err = strconv.ParseBool(decryptedValue)
case "time":
var value time.Time
err = value.UnmarshalText(decryptedBytes)
plaintext = value
case "comment":
plaintext = sops.Comment{Value: decryptedValue}
default:
Expand Down Expand Up @@ -176,6 +181,12 @@ func (c Cipher) Encrypt(plaintext interface{}, key []byte, additionalData string
} else {
plainBytes = []byte("False")
}
case time.Time:
encryptedType = "time"
plainBytes, err = value.MarshalText()
if err != nil {
return "", fmt.Errorf("Error marshaling timestamp %q: %w", value, err)
}
case sops.Comment:
encryptedType = "comment"
plainBytes = []byte(value.Value)
Expand Down
93 changes: 92 additions & 1 deletion aes/cipher_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package aes

import (
"bytes"
"crypto/rand"
"reflect"
"strings"
"testing"
"testing/quick"
"time"

"github.com/stretchr/testify/assert"
"github.com/getsops/sops/v3"
"github.com/stretchr/testify/assert"
)

func TestDecrypt(t *testing.T) {
Expand Down Expand Up @@ -108,6 +111,36 @@ func TestRoundtripBool(t *testing.T) {
}
}

func TestRoundtripTime(t *testing.T) {
key := []byte(strings.Repeat("f", 32))
parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
assert.Nil(t, err)
loc := time.FixedZone("", 12300) // offset must be divisible by 60, otherwise won't survive a round-trip
values := []time.Time{
time.UnixMilli(0).In(time.UTC),
time.UnixMilli(123456).In(time.UTC),
time.UnixMilli(123456).In(loc),
time.UnixMilli(123456789).In(time.UTC),
time.UnixMilli(123456789).In(loc),
time.UnixMilli(1234567890).In(time.UTC),
time.UnixMilli(1234567890).In(loc),
parsedTime,
}
for _, value := range values {
s, err := NewCipher().Encrypt(value, key, "foo")
assert.Nil(t, err)
if err != nil {
continue
}
d, err := NewCipher().Decrypt(s, key, "foo")
assert.Nil(t, err)
if err != nil {
continue
}
assert.Equal(t, value, d)
}
}

func TestEncryptEmptyComment(t *testing.T) {
key := []byte(strings.Repeat("f", 32))
s, err := NewCipher().Encrypt(sops.Comment{}, key, "")
Expand All @@ -121,3 +154,61 @@ func TestDecryptEmptyValue(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "", s)
}

// This test would belong more in sops_test.go, but from there we cannot access
// the aes package to get a cipher which can actually handle time.Time objects.
func TestTimestamps(t *testing.T) {
unixTime := time.UnixMilli(123456789).In(time.UTC)
parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
assert.Nil(t, err)
branches := sops.TreeBranches{
sops.TreeBranch{
sops.TreeItem{
Key: "foo",
Value: unixTime,
},
sops.TreeItem{
Key: "bar",
Value: sops.TreeBranch{
sops.TreeItem{
Key: "foo",
Value: parsedTime,
},
},
},
},
}
tree := sops.Tree{Branches: branches, Metadata: sops.Metadata{UnencryptedSuffix: "_unencrypted"}}
expected := sops.TreeBranch{
sops.TreeItem{
Key: "foo",
Value: unixTime,
},
sops.TreeItem{
Key: "bar",
Value: sops.TreeBranch{
sops.TreeItem{
Key: "foo",
Value: parsedTime,
},
},
},
}
cipher := NewCipher()
_, err = tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the tree failed: %s", err)
}
if reflect.DeepEqual(tree.Branches[0], expected) {
t.Errorf("Trees do match: \ngot \t\t%+v,\n not expected \t\t%+v", tree.Branches[0], expected)
}
_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Decrypting the tree failed: %s", err)
}
assert.Equal(t, tree.Branches[0][0].Value, unixTime)
assert.Equal(t, tree.Branches[0], expected)
if !reflect.DeepEqual(tree.Branches[0], expected) {
t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected)
}
}
44 changes: 44 additions & 0 deletions functional-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,50 @@ b: ba"#
}
}

#[test]
fn test_yaml_time() {
let file_path = prepare_temp_file(
"test_time.yaml",
r#"a: 2024-01-01
b: 2006-01-02T15:04:05+07:06"#
.as_bytes(),
);
assert!(
Command::new(SOPS_BINARY_PATH)
.arg("encrypt")
.arg("-i")
.arg(file_path.clone())
.output()
.expect("Error running sops")
.status
.success(),
"sops didn't exit successfully"
);
let output = Command::new(SOPS_BINARY_PATH)
.arg("decrypt")
.arg("-i")
.arg(file_path.clone())
.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 exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert_eq!(
s,
r#"a: 2024-01-01T00:00:00Z
b: 2006-01-02T15:04:05+07:06
"#
);
}

#[test]
fn unset_json_file() {
// Test removal of tree branch
Expand Down
4 changes: 4 additions & 0 deletions sops.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ func (branch TreeBranch) walkValue(in interface{}, path []string, commentsStack
return onLeaves(in, path, commentsStack)
case float64:
return onLeaves(in, path, commentsStack)
case time.Time:
return onLeaves(in, path, commentsStack)
case Comment:
return onLeaves(in, path, commentsStack)
case TreeBranch:
Expand Down Expand Up @@ -968,6 +970,8 @@ func ToBytes(in interface{}) ([]byte, error) {
return boolB, nil
case []byte:
return in, nil
case time.Time:
return in.MarshalText()
case Comment:
return ToBytes(in.Value)
default:
Expand Down
Loading