diff --git a/README.rst b/README.rst index ff8870a01..b944d6ce0 100644 --- a/README.rst +++ b/README.rst @@ -197,6 +197,8 @@ configuration directory. On Linux, this would be ``$XDG_CONFIG_HOME/sops/age/key On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``. On Windows, this would be ``%AppData%\sops\age\keys.txt``. You can specify the location of this file manually by setting the environment variable **SOPS_AGE_KEY_FILE**. +Alternatively you can provide the the key(s) directly by setting the **SOPS_AGE_KEY** +environment variable. The contents of this key file should be a list of age X25519 identities, one per line. Lines beginning with ``#`` are considered comments and ignored. Each diff --git a/age/keysource.go b/age/keysource.go index 05a85a343..e9e874ddf 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -20,6 +20,9 @@ func init() { log = logging.NewLogger("AGE") } +const SopsAgeKeyEnv = "SOPS_AGE_KEY" +const SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE" + // MasterKey is an age key used to encrypt and decrypt sops' data key. type MasterKey struct { Identity string // a Bech32-encoded private key @@ -93,27 +96,46 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { // Decrypt decrypts the EncryptedKey field with the age identity and returns the result. func (key *MasterKey) Decrypt() ([]byte, error) { - ageKeyFilePath, ok := os.LookupEnv("SOPS_AGE_KEY_FILE") + var ageKeyReader io.Reader + var ageKeyReaderName string + + if ageKeyReader == nil { + ageKey, ok := os.LookupEnv(SopsAgeKeyEnv) + if ok { + ageKeyReader = strings.NewReader(ageKey) + ageKeyReaderName = "environment variable" + } + } - if !ok { - userConfigDir, err := os.UserConfigDir() + if ageKeyReader == nil { + ageKeyFilePath, ok := os.LookupEnv(SopsAgeKeyFileEnv) + if ok { + ageKeyFile, err := os.Open(ageKeyFilePath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer ageKeyFile.Close() + ageKeyReader = ageKeyFile + ageKeyReaderName = ageKeyFilePath + } + } + if ageKeyReader == nil { + userConfigDir, err := os.UserConfigDir() if err != nil { return nil, fmt.Errorf("user config directory could not be determined: %w", err) } - - ageKeyFilePath = filepath.Join(userConfigDir, "sops", "age", "keys.txt") - } - - ageKeyFile, err := os.Open(ageKeyFilePath) - - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) + ageKeyFilePath := filepath.Join(userConfigDir, "sops", "age", "keys.txt") + ageKeyFile, err := os.Open(ageKeyFilePath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer ageKeyFile.Close() + ageKeyReader = ageKeyFile + ageKeyReaderName = ageKeyFilePath } - defer ageKeyFile.Close() - - identities, err := age.ParseIdentities(ageKeyFile) + identities, err := age.ParseIdentities(ageKeyReader) if err != nil { return nil, err @@ -124,7 +146,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { r, err := age.Decrypt(ar, identities...) if err != nil { - return nil, fmt.Errorf("no age identity found in %q that could decrypt the data", ageKeyFilePath) + return nil, fmt.Errorf("no age identity found in %q that could decrypt the data", ageKeyReaderName) } var b bytes.Buffer diff --git a/age/keysource_test.go b/age/keysource_test.go index 35891b20f..0b30b355d 100644 --- a/age/keysource_test.go +++ b/age/keysource_test.go @@ -1,6 +1,7 @@ package age import ( + "io/ioutil" "os" "path" "runtime" @@ -44,7 +45,7 @@ func TestAge(t *testing.T) { assert.NoError(err) _, filename, _, _ := runtime.Caller(0) - err = os.Setenv("SOPS_AGE_KEY_FILE", path.Join(path.Dir(filename), "keys.txt")) + err = os.Setenv(SopsAgeKeyFileEnv, path.Join(path.Dir(filename), "keys.txt")) assert.NoError(err) decryptedKey, err := key.Decrypt() @@ -70,7 +71,33 @@ DOMAIN=files.127.0.0.1.nip.io` assert.NoError(err) _, filename, _, _ := runtime.Caller(0) - err = os.Setenv("SOPS_AGE_KEY_FILE", path.Join(path.Dir(filename), "keys.txt")) + err = os.Setenv(SopsAgeKeyFileEnv, path.Join(path.Dir(filename), "keys.txt")) + defer os.Unsetenv(SopsAgeKeyFileEnv) + assert.NoError(err) + + decryptedKey, err := key.Decrypt() + assert.NoError(err) + assert.Equal(dataKey, decryptedKey) +} + +func TestAgeEnv(t *testing.T) { + assert := assert.New(t) + + key, err := MasterKeyFromRecipient("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw") + + assert.NoError(err) + assert.Equal("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw", key.ToString()) + + dataKey := []byte("abcdefghijklmnopqrstuvwxyz123456") + + err = key.Encrypt(dataKey) + assert.NoError(err) + + _, filename, _, _ := runtime.Caller(0) + keysBytes, err := ioutil.ReadFile(path.Join(path.Dir(filename), "keys.txt")) + assert.NoError(err) + err = os.Setenv(SopsAgeKeyEnv, string(keysBytes)) + defer os.Unsetenv(SopsAgeKeyEnv) assert.NoError(err) decryptedKey, err := key.Decrypt()