diff --git a/Makefile b/Makefile index 1712c17d3..e0fbd3cbb 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ checkmd: $(MD_FILES) .PHONY: test test: vendor gpg --import pgp/sops_functional_tests_key.asc 2>&1 1>/dev/null || exit 0 - unset SOPS_AGE_KEY_FILE; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... + unset SOPS_AGE_KEY_FILE; unset SOPS_AGE_KEY_CMD; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... .PHONY: showcoverage showcoverage: test diff --git a/README.rst b/README.rst index 422af5650..e930bf229 100644 --- a/README.rst +++ b/README.rst @@ -227,7 +227,8 @@ On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``. 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 key(s) directly by setting the **SOPS_AGE_KEY** -environment variable. +environment variable. Alternatively, you can provide a command to output the age keys +by setting the **SOPS_AGE_KEY_CMD** 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 f5f168576..f77ab738d 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -4,10 +4,10 @@ import ( "bufio" "bytes" "errors" - "filippo.io/age/plugin" "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -15,9 +15,11 @@ import ( "filippo.io/age" "filippo.io/age/agessh" "filippo.io/age/armor" + "filippo.io/age/plugin" "github.com/sirupsen/logrus" "github.com/getsops/sops/v3/logging" + "github.com/google/shlex" ) const ( @@ -27,6 +29,9 @@ const ( // SopsAgeKeyFileEnv can be set as an environment variable pointing to an // age keys file. SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE" + // SopsAgeKeyCmdEnv can be set as an environment variable with a command + // to execute that returns the age keys. + SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD" // SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to // a private SSH key file. SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE" @@ -310,6 +315,18 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { readers[SopsAgeKeyFileEnv] = f } + if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok { + args, err := shlex.Split(ageKeyCmd) + if err != nil { + return nil, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err) + } + out, err := exec.Command(args[0], args[1:]...).Output() + if err != nil { + return nil, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err) + } + readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out) + } + userConfigDir, err := getUserConfigDir() if err != nil && len(readers) == 0 && len(identities) == 0 { return nil, fmt.Errorf("user config directory could not be determined: %w", err) diff --git a/age/keysource_test.go b/age/keysource_test.go index 39aef834d..42bdf3c6a 100644 --- a/age/keysource_test.go +++ b/age/keysource_test.go @@ -485,6 +485,33 @@ func TestMasterKey_loadIdentities(t *testing.T) { assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv)) assert.Nil(t, got) }) + + t.Run(SopsAgeKeyCmdEnv, func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "echo '"+mockIdentity+"'") + + key := &MasterKey{} + got, err := key.loadIdentities() + assert.NoError(t, err) + assert.Len(t, got, 1) + }) + + t.Run("cmd error", func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "meow") + + key := &MasterKey{} + got, err := key.loadIdentities() + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to execute command meow") + assert.Nil(t, got) + }) } // overwriteUserConfigDir sets the user config directory and the user home directory