1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00

Encrypt and decrypt time.Time objects.

Signed-off-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Felix Fontein
2025-02-15 23:11:13 +01:00
parent 45cdd26c96
commit d887433f75
4 changed files with 151 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/logging"
@@ -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:
@@ -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)

View File

@@ -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) {
@@ -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, "")
@@ -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)
}
}

View File

@@ -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

View File

@@ -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:
@@ -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: