зеркало из https://github.com/getsops/sops.git
403 строки
12 KiB
Go
403 строки
12 KiB
Go
package age
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
const (
|
|
// mockRecipient is a mock age recipient, it matches mockIdentity.
|
|
mockRecipient string = "age1lzd99uklcjnc0e7d860axevet2cz99ce9pq6tzuzd05l5nr28ams36nvun"
|
|
// mockIdentity is a mock age identity.
|
|
mockIdentity string = "AGE-SECRET-KEY-1G0Q5K9TV4REQ3ZSQRMTMG8NSWQGYT0T7TZ33RAZEE0GZYVZN0APSU24RK7"
|
|
// mockOtherIdentity is another mock age identity.
|
|
mockOtherIdentity string = "AGE-SECRET-KEY-1432K5YRNSC44GC4986NXMX6GVZ52WTMT9C79CLUVWYY4DKDHD5JSNDP4MC"
|
|
// mockEncryptedKey equals to mockEncryptedKeyPlain when decrypted with mockIdentity.
|
|
mockEncryptedKey string = `-----BEGIN AGE ENCRYPTED FILE-----
|
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY2t2NkdLUGRvY3l2OGNy
|
|
MVJWcUhCOEZrUG8yeCtnRnhxL0I5NFk4YjJFCmE4SVQ3MEdyZkFqRWpSa2F0NVhF
|
|
VDUybzBxdS9nSGpHSVRVMUI0UEVqZkkKLS0tIGJjeGhNQ0Y5L2VZRVVYSm90djFF
|
|
bzdnQ3UwTGljMmtrbWNMV1MxYkFzUFUK4xjOZOTGdcbzuwUY/zeBXhcF+Md3e5PQ
|
|
EylloI7MNGbadPGb
|
|
-----END AGE ENCRYPTED FILE-----`
|
|
// mockEncryptedKeyPlain is the plain value of mockEncryptedKey.
|
|
mockEncryptedKeyPlain string = "data"
|
|
)
|
|
|
|
func TestMasterKeysFromRecipients(t *testing.T) {
|
|
const otherRecipient = "age1tmaae3ld5vpevmsh5yacsauzx8jetg300mpvc4ugp5zr5l6ssq9sla97ep"
|
|
|
|
t.Run("recipient", func(t *testing.T) {
|
|
got, err := MasterKeysFromRecipients(mockRecipient)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, got, 1)
|
|
assert.Equal(t, got[0].Recipient, mockRecipient)
|
|
})
|
|
|
|
t.Run("recipients", func(t *testing.T) {
|
|
got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, got, 2)
|
|
assert.Equal(t, got[0].Recipient, mockRecipient)
|
|
assert.Equal(t, got[1].Recipient, otherRecipient)
|
|
})
|
|
|
|
t.Run("leading and trailing spaces", func(t *testing.T) {
|
|
got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " ")
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, got, 2)
|
|
assert.Equal(t, got[0].Recipient, mockRecipient)
|
|
assert.Equal(t, got[1].Recipient, otherRecipient)
|
|
})
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
got, err := MasterKeysFromRecipients("")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 0)
|
|
})
|
|
}
|
|
|
|
func TestMasterKeyFromRecipient(t *testing.T) {
|
|
t.Run("recipient", func(t *testing.T) {
|
|
got, err := MasterKeyFromRecipient(mockRecipient)
|
|
assert.NoError(t, err)
|
|
assert.EqualValues(t, mockRecipient, got.Recipient)
|
|
assert.NotNil(t, got.parsedRecipient)
|
|
assert.Nil(t, got.parsedIdentities)
|
|
})
|
|
|
|
t.Run("leading and trailing spaces", func(t *testing.T) {
|
|
got, err := MasterKeyFromRecipient(" " + mockRecipient + " ")
|
|
assert.NoError(t, err)
|
|
assert.EqualValues(t, mockRecipient, got.Recipient)
|
|
assert.NotNil(t, got.parsedRecipient)
|
|
assert.Nil(t, got.parsedIdentities)
|
|
})
|
|
|
|
t.Run("invalid recipient", func(t *testing.T) {
|
|
got, err := MasterKeyFromRecipient("invalid")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, got)
|
|
})
|
|
}
|
|
|
|
func TestParsedIdentities_Import(t *testing.T) {
|
|
i := make(ParsedIdentities, 0)
|
|
assert.NoError(t, i.Import(mockIdentity, mockOtherIdentity))
|
|
assert.Len(t, i, 2)
|
|
|
|
assert.Error(t, i.Import("invalid"))
|
|
assert.Len(t, i, 2)
|
|
}
|
|
|
|
func TestParsedIdentities_ApplyToMasterKey(t *testing.T) {
|
|
i := make(ParsedIdentities, 0)
|
|
assert.NoError(t, i.Import(mockIdentity, mockOtherIdentity))
|
|
|
|
key := &MasterKey{}
|
|
i.ApplyToMasterKey(key)
|
|
assert.EqualValues(t, key.parsedIdentities, i)
|
|
}
|
|
|
|
func TestMasterKey_Encrypt(t *testing.T) {
|
|
mockParsedRecipient, err := parseRecipient(mockRecipient)
|
|
assert.NoError(t, err)
|
|
|
|
t.Run("recipient", func(t *testing.T) {
|
|
key := &MasterKey{
|
|
Recipient: mockRecipient,
|
|
}
|
|
assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain)))
|
|
assert.NotEmpty(t, key.EncryptedKey)
|
|
})
|
|
|
|
t.Run("parsed recipient", func(t *testing.T) {
|
|
key := &MasterKey{
|
|
parsedRecipient: mockParsedRecipient,
|
|
}
|
|
assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain)))
|
|
assert.NotEmpty(t, key.EncryptedKey)
|
|
})
|
|
|
|
t.Run("invalid recipient", func(t *testing.T) {
|
|
key := &MasterKey{
|
|
Recipient: "invalid",
|
|
}
|
|
err := key.Encrypt([]byte(mockEncryptedKeyPlain))
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "failed to parse input as Bech32-encoded age public key")
|
|
assert.Empty(t, key.EncryptedKey)
|
|
})
|
|
|
|
t.Run("parsed recipient and invalid recipient", func(t *testing.T) {
|
|
key := &MasterKey{
|
|
Recipient: "invalid",
|
|
parsedRecipient: mockParsedRecipient,
|
|
}
|
|
// Validates mockParsedRecipient > Recipient
|
|
assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain)))
|
|
assert.NotEmpty(t, key.EncryptedKey)
|
|
})
|
|
}
|
|
|
|
func TestMasterKey_EncryptIfNeeded(t *testing.T) {
|
|
key, err := MasterKeyFromRecipient(mockRecipient)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, key.EncryptIfNeeded([]byte(mockEncryptedKeyPlain)))
|
|
|
|
encryptedKey := key.EncryptedKey
|
|
assert.Contains(t, encryptedKey, "AGE ENCRYPTED FILE")
|
|
|
|
assert.NoError(t, key.EncryptIfNeeded([]byte("some other data")))
|
|
assert.Equal(t, encryptedKey, key.EncryptedKey)
|
|
}
|
|
|
|
func TestMasterKey_EncryptedDataKey(t *testing.T) {
|
|
key := &MasterKey{EncryptedKey: "some key"}
|
|
assert.EqualValues(t, key.EncryptedKey, key.EncryptedDataKey())
|
|
}
|
|
|
|
func TestMasterKey_Decrypt(t *testing.T) {
|
|
t.Run("parsed identities", func(t *testing.T) {
|
|
key := &MasterKey{EncryptedKey: mockEncryptedKey}
|
|
var ids ParsedIdentities
|
|
assert.NoError(t, ids.Import(mockOtherIdentity, mockIdentity))
|
|
ids.ApplyToMasterKey(key)
|
|
|
|
got, err := key.Decrypt()
|
|
assert.NoError(t, err)
|
|
assert.EqualValues(t, mockEncryptedKeyPlain, got)
|
|
})
|
|
|
|
t.Run("loaded identities", func(t *testing.T) {
|
|
key := &MasterKey{EncryptedKey: mockEncryptedKey}
|
|
t.Setenv(SopsAgeKeyEnv, mockIdentity)
|
|
|
|
got, err := key.Decrypt()
|
|
assert.NoError(t, err)
|
|
assert.EqualValues(t, mockEncryptedKeyPlain, got)
|
|
})
|
|
|
|
t.Run("no identities", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
key := &MasterKey{EncryptedKey: mockEncryptedKey}
|
|
got, err := key.Decrypt()
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "failed to load age identities")
|
|
assert.Nil(t, got)
|
|
})
|
|
|
|
t.Run("no matching identity", func(t *testing.T) {
|
|
key := &MasterKey{EncryptedKey: mockEncryptedKey}
|
|
var ids ParsedIdentities
|
|
assert.NoError(t, ids.Import(mockOtherIdentity))
|
|
ids.ApplyToMasterKey(key)
|
|
|
|
// This confirms lazy-loading works as intended
|
|
t.Setenv(SopsAgeKeyEnv, mockIdentity)
|
|
|
|
got, err := key.Decrypt()
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "no identity matched any of the recipients")
|
|
assert.Nil(t, got)
|
|
})
|
|
|
|
t.Run("invalid encrypted key", func(t *testing.T) {
|
|
key := &MasterKey{EncryptedKey: "invalid"}
|
|
t.Setenv(SopsAgeKeyEnv, mockIdentity)
|
|
|
|
got, err := key.Decrypt()
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "failed to create reader for decrypting sops data key with age")
|
|
assert.Nil(t, got)
|
|
})
|
|
}
|
|
|
|
func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) {
|
|
encryptKey, err := MasterKeyFromRecipient(mockRecipient)
|
|
assert.NoError(t, err)
|
|
|
|
data := []byte("some secret data")
|
|
assert.NoError(t, encryptKey.Encrypt(data))
|
|
assert.NotEmpty(t, encryptKey.EncryptedKey)
|
|
|
|
var ids ParsedIdentities
|
|
assert.NoError(t, ids.Import(mockIdentity))
|
|
|
|
decryptKey := &MasterKey{}
|
|
decryptKey.EncryptedKey = encryptKey.EncryptedKey
|
|
ids.ApplyToMasterKey(decryptKey)
|
|
|
|
decryptedData, err := decryptKey.Decrypt()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, data, decryptedData)
|
|
}
|
|
|
|
func TestMasterKey_NeedsRotation(t *testing.T) {
|
|
key := &MasterKey{Recipient: mockRecipient}
|
|
assert.False(t, key.NeedsRotation())
|
|
}
|
|
|
|
func TestMasterKey_ToString(t *testing.T) {
|
|
key := &MasterKey{Recipient: mockRecipient}
|
|
assert.Equal(t, key.Recipient, key.ToString())
|
|
}
|
|
|
|
func TestMasterKey_ToMap(t *testing.T) {
|
|
key := &MasterKey{
|
|
Recipient: mockRecipient,
|
|
EncryptedKey: "some-encrypted-key",
|
|
}
|
|
assert.Equal(t, map[string]interface{}{
|
|
"recipient": mockRecipient,
|
|
"enc": key.EncryptedKey,
|
|
}, key.ToMap())
|
|
}
|
|
|
|
func TestMasterKey_loadIdentities(t *testing.T) {
|
|
t.Run(SopsAgeKeyEnv, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Overwrite to ensure local config is not picked up by tests
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
t.Setenv(SopsAgeKeyEnv, mockIdentity)
|
|
|
|
key := &MasterKey{}
|
|
got, err := key.loadIdentities()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 1)
|
|
})
|
|
|
|
t.Run(SopsAgeKeyEnv+" multiple", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Overwrite to ensure local config is not picked up by tests
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
t.Setenv(SopsAgeKeyEnv, mockIdentity+"\n"+mockOtherIdentity)
|
|
|
|
key := &MasterKey{}
|
|
got, err := key.loadIdentities()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 2)
|
|
})
|
|
|
|
t.Run(SopsAgeKeyFileEnv, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Overwrite to ensure local config is not picked up by tests
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
keyPath := filepath.Join(tmpDir, "keys.txt")
|
|
assert.NoError(t, os.WriteFile(keyPath, []byte(mockIdentity), 0o644))
|
|
|
|
t.Setenv(SopsAgeKeyFileEnv, keyPath)
|
|
|
|
key := &MasterKey{}
|
|
got, err := key.loadIdentities()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 1)
|
|
})
|
|
|
|
t.Run(SopsAgeKeyUserConfigPath, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
// We need to use getUserConfigDir and not tmpDir as it may add a suffix
|
|
cfgDir, err := getUserConfigDir()
|
|
assert.NoError(t, err)
|
|
keyPath := filepath.Join(cfgDir, SopsAgeKeyUserConfigPath)
|
|
assert.True(t, strings.HasPrefix(keyPath, cfgDir))
|
|
|
|
assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700))
|
|
assert.NoError(t, os.WriteFile(keyPath, []byte(mockIdentity), 0o644))
|
|
|
|
got, err := (&MasterKey{}).loadIdentities()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 1)
|
|
})
|
|
|
|
t.Run("no identity", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
got, err := (&MasterKey{}).loadIdentities()
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "failed to open file")
|
|
assert.Nil(t, got)
|
|
})
|
|
|
|
t.Run("multiple identities", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
// We need to use getUserConfigDir and not tmpDir as it may add a suffix
|
|
cfgDir, err := getUserConfigDir()
|
|
assert.NoError(t, err)
|
|
keyPath1 := filepath.Join(cfgDir, SopsAgeKeyUserConfigPath)
|
|
assert.True(t, strings.HasPrefix(keyPath1, cfgDir))
|
|
|
|
assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath1), 0o700))
|
|
assert.NoError(t, os.WriteFile(keyPath1, []byte(mockIdentity), 0o644))
|
|
|
|
keyPath2 := filepath.Join(tmpDir, "keys.txt")
|
|
assert.NoError(t, os.WriteFile(keyPath2, []byte(mockOtherIdentity), 0o644))
|
|
t.Setenv(SopsAgeKeyFileEnv, keyPath2)
|
|
|
|
got, err := (&MasterKey{}).loadIdentities()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got, 2)
|
|
})
|
|
|
|
t.Run("parsing error", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Overwrite to ensure local config is not picked up by tests
|
|
overwriteUserConfigDir(t, tmpDir)
|
|
|
|
t.Setenv(SopsAgeKeyEnv, "invalid")
|
|
|
|
key := &MasterKey{}
|
|
got, err := key.loadIdentities()
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv))
|
|
assert.Nil(t, got)
|
|
})
|
|
}
|
|
|
|
// overwriteUserConfigDir sets the user config directory based on the
|
|
// os.UserConfigDir logic.
|
|
func overwriteUserConfigDir(t *testing.T, path string) {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
t.Setenv("AppData", path)
|
|
case "plan9": // This adds "/lib" as a suffix to $home
|
|
t.Setenv("home", path)
|
|
default: // Unix
|
|
t.Setenv("XDG_CONFIG_HOME", path)
|
|
}
|
|
}
|
|
|
|
// Make sure that on all supported platforms but Windows, XDG_CONFIG_HOME
|
|
// can be used to specify the user's home directory. For most platforms
|
|
// this is handled by Go's os.UserConfigDir(), but for Darwin our code
|
|
// in getUserConfigDir() handles this explicitly.
|
|
func TestUserConfigDir(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
const dir = "/test/home/dir"
|
|
t.Setenv("XDG_CONFIG_HOME", dir)
|
|
home, err := getUserConfigDir()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, home, dir)
|
|
}
|
|
}
|