diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index a97e98426..3d2c15205 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -22,11 +22,10 @@ import ( "encoding/base64" "fmt" "os" - "os/exec" "path/filepath" "strings" - securejoin "github.com/cyphar/filepath-securejoin" + "github.com/fluxcd/kustomize-controller/internal/sops/pgp" "go.mozilla.org/sops/v3" "go.mozilla.org/sops/v3/aes" "go.mozilla.org/sops/v3/cmd/sops/common" @@ -59,29 +58,29 @@ type KustomizeDecryptor struct { client.Client kustomization kustomizev1.Kustomization - homeDir string + gnuPGHome pgp.GnuPGHome ageIdentities []string vaultToken string azureAADConfig *azkv.AADConfig } func NewDecryptor(kubeClient client.Client, - kustomization kustomizev1.Kustomization, homeDir string) *KustomizeDecryptor { + kustomization kustomizev1.Kustomization, gnuPGHome string) *KustomizeDecryptor { return &KustomizeDecryptor{ Client: kubeClient, kustomization: kustomization, - homeDir: homeDir, + gnuPGHome: pgp.GnuPGHome(gnuPGHome), } } func NewTempDecryptor(kubeClient client.Client, kustomization kustomizev1.Kustomization) (*KustomizeDecryptor, func(), error) { - tmpDir, err := os.MkdirTemp("", fmt.Sprintf("decryptor-%s-", kustomization.Name)) + gnuPGHome, err := pgp.NewGnuPGHome() if err != nil { - return nil, nil, fmt.Errorf("tmp dir error: %w", err) + return nil, nil, fmt.Errorf("cannot create decryptor: %w", err) } - cleanup := func() { os.RemoveAll(tmpDir) } - return NewDecryptor(kubeClient, kustomization, tmpDir), cleanup, nil + cleanup := func() { os.RemoveAll(gnuPGHome.String()) } + return NewDecryptor(kubeClient, kustomization, gnuPGHome.String()), cleanup, nil } func (kd *KustomizeDecryptor) Decrypt(res *resource.Resource) (*resource.Resource, error) { @@ -162,15 +161,8 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { for name, value := range secret.Data { switch filepath.Ext(name) { case ".asc": - keyPath, err := securejoin.SecureJoin(tmpDir, name) - if err != nil { - return err - } - if err := os.WriteFile(keyPath, value, os.ModePerm); err != nil { - return fmt.Errorf("unable to write key to storage: %w", err) - } - if err := kd.gpgImport(keyPath); err != nil { - return err + if err := kd.gnuPGHome.Import(value); err != nil { + return fmt.Errorf("failed to import '%s' Secret data: %w", name, err) } case ".agekey": ageIdentities = append(ageIdentities, string(value)) @@ -200,19 +192,6 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { return nil } -func (kd *KustomizeDecryptor) gpgImport(path string) error { - args := []string{"--batch", "--import", path} - if kd.homeDir != "" { - args = append([]string{"--homedir", kd.homeDir}, args...) - } - cmd := exec.Command("gpg", args...) - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("gpg import error: %s", string(out)) - } - return nil -} - func (kd *KustomizeDecryptor) decryptDotEnvFiles(dirpath string) error { kustomizePath := filepath.Join(dirpath, konfig.DefaultKustomizationFileName()) ksData, err := os.ReadFile(kustomizePath) @@ -285,7 +264,7 @@ func (kd KustomizeDecryptor) DataWithFormat(data []byte, inputFormat, outputForm } serverOpts := []intkeyservice.ServerOption{ - intkeyservice.WithHomeDir(kd.homeDir), + intkeyservice.WithGnuPGHome(kd.gnuPGHome), intkeyservice.WithVaultToken(kd.vaultToken), intkeyservice.WithAgePrivateKeys(kd.ageIdentities), } diff --git a/internal/sops/keyservice/options.go b/internal/sops/keyservice/options.go index 936700b52..dee0202be 100644 --- a/internal/sops/keyservice/options.go +++ b/internal/sops/keyservice/options.go @@ -19,6 +19,7 @@ package keyservice import ( "github.com/fluxcd/kustomize-controller/internal/sops/azkv" "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" + "github.com/fluxcd/kustomize-controller/internal/sops/pgp" "go.mozilla.org/sops/v3/keyservice" ) @@ -29,12 +30,12 @@ type ServerOption interface { ApplyToServer(s *Server) } -// WithHomeDir configures the contained "home directory" on the Server. -type WithHomeDir string +// WithGnuPGHome configures the GnuPG home directory on the Server. +type WithGnuPGHome string // ApplyToServer applies this configuration to the given Server. -func (o WithHomeDir) ApplyToServer(s *Server) { - s.homeDir = string(o) +func (o WithGnuPGHome) ApplyToServer(s *Server) { + s.gnuPGHome = pgp.GnuPGHome(o) } // WithVaultToken configures the Hashicorp Vault token on the Server. diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 9b3bbc7f9..2bcae53aa 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -25,9 +25,11 @@ import ( // environment. Any request not handled by the Server is forwarded to the // embedded default server. type Server struct { - // homeDir is the contained "home directory" used for the Encrypt and - // Decrypt operations for certain key types, e.g. PGP. - homeDir string + // gnuPGHome is the GnuPG home directory used for the Encrypt and Decrypt + // operations for PGP key types. + // When empty, the requests will be handled using the systems' runtime + // keyring. + gnuPGHome pgp.GnuPGHome // agePrivateKeys holds the private keys used for Encrypt and Decrypt // operations of age requests. @@ -50,7 +52,7 @@ type Server struct { // NewServer constructs a new Server, configuring it with the provided options // before returning the result. -// When WithDefaultServer is not provided as an option, the SOPS server +// When WithDefaultServer() is not provided as an option, the SOPS server // implementation is configured as default. func NewServer(options ...ServerOption) keyservice.KeyServiceServer { s := &Server{} @@ -152,11 +154,10 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* } func (ks *Server) encryptWithPgp(key *keyservice.PgpKey, plaintext []byte) ([]byte, error) { - if ks.homeDir == "" { - return nil, status.Errorf(codes.Unimplemented, "PGP encrypt service unavailable: missing home dir configuration") + pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint) + if ks.gnuPGHome != "" { + ks.gnuPGHome.ApplyToMasterKey(pgpKey) } - - pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint, ks.homeDir) err := pgpKey.Encrypt(plaintext) if err != nil { return nil, err @@ -165,11 +166,10 @@ func (ks *Server) encryptWithPgp(key *keyservice.PgpKey, plaintext []byte) ([]by } func (ks *Server) decryptWithPgp(key *keyservice.PgpKey, ciphertext []byte) ([]byte, error) { - if ks.homeDir == "" { - return nil, status.Errorf(codes.Unimplemented, "PGP decrypt service unavailable: missing home dir configuration") + pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint) + if ks.gnuPGHome != "" { + ks.gnuPGHome.ApplyToMasterKey(pgpKey) } - - pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint, ks.homeDir) pgpKey.EncryptedKey = string(ciphertext) plaintext, err := pgpKey.Decrypt() return plaintext, err diff --git a/internal/sops/pgp/keysource.go b/internal/sops/pgp/keysource.go index 204c8564c..47fc15438 100644 --- a/internal/sops/pgp/keysource.go +++ b/internal/sops/pgp/keysource.go @@ -49,16 +49,100 @@ type MasterKey struct { // needs rotation. CreationDate time.Time - homeDir string + // gnuPGHomeDir contains the absolute path to a GnuPG home directory. + // It can be injected by a (local) keyservice.KeyServiceServer using + // GnuPGHome.ApplyToMasterKey(). + gnuPGHomeDir string } // MasterKeyFromFingerprint takes a PGP fingerprint and returns a // new MasterKey with that fingerprint. -func MasterKeyFromFingerprint(fingerprint, homeDir string) *MasterKey { +func MasterKeyFromFingerprint(fingerprint string) *MasterKey { return &MasterKey{ Fingerprint: strings.Replace(fingerprint, " ", "", -1), CreationDate: time.Now().UTC(), - homeDir: homeDir, + } +} + +// GnuPGHome is the absolute path to a GnuPG home directory. +// A new keyring can be constructed by combining the use of NewGnuPGHome() and +// Import() or ImportFile(). +type GnuPGHome string + +// NewGnuPGHome initializes a new GnuPGHome in a temporary directory. +// The caller is expected to handle the garbage collection of the created +// directory. +func NewGnuPGHome() (GnuPGHome, error) { + tmpDir, err := os.MkdirTemp("", "sops-gnupghome-") + if err != nil { + return "", fmt.Errorf("failed to create new GnuPG home: %w", err) + } + return GnuPGHome(tmpDir), nil +} + +// Import attempts to import the armored key bytes into the GnuPGHome keyring. +// It returns an error if the GnuPGHome does not pass Validate, or if the +// import failed. +func (d GnuPGHome) Import(armoredKey []byte) error { + if err := d.Validate(); err != nil { + return fmt.Errorf("cannot import armored key data into GnuPG keyring: %w", err) + } + + args := []string{"--batch", "--import"} + err, _, stderr := gpgExec(d.String(), args, bytes.NewReader(armoredKey)) + if err != nil { + return fmt.Errorf("failed to import armored key data into GnuPG keyring: %s", strings.TrimSpace(stderr.String())) + } + return nil +} + +// ImportFile attempts to import the armored key file into the GnuPGHome +// keyring. +// It returns an error if the GnuPGHome does not pass Validate, or if the +// import failed. +func (d GnuPGHome) ImportFile(path string) error { + b, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("cannot read armored key data from file: %w", err) + } + return d.Import(b) +} + +// Validate ensures the GnuPGHome is a valid GnuPG home directory path. +// When validation fails, it returns a descriptive reason as error. +func (d GnuPGHome) Validate() error { + if d == "" { + return fmt.Errorf("empty GNUPGHOME path") + } + if !filepath.IsAbs(d.String()) { + return fmt.Errorf("GNUPGHOME must be an absolute path") + } + fi, err := os.Lstat(d.String()) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("GNUPGHOME does not exist") + } + return fmt.Errorf("cannot stat GNUPGHOME: %w", err) + } + if !fi.IsDir() { + return fmt.Errorf("GNUGPHOME is not a directory") + } + if perm := fi.Mode().Perm(); perm != 0o700 { + return fmt.Errorf("GNUPGHOME has invalid permissions: got %#o wanted %#o", perm, 0o700) + } + return nil +} + +// String returns the GnuPGHome as a string. It does not Validate. +func (d GnuPGHome) String() string { + return string(d) +} + +// ApplyToMasterKey configures the GnuPGHome on the provided key if it passes +// Validate. +func (d GnuPGHome) ApplyToMasterKey(key *MasterKey) { + if err := d.Validate(); err == nil { + key.gnuPGHomeDir = d.String() } } @@ -78,7 +162,7 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { fingerprint, "--no-encrypt-to", } - err, stdout, stderr := gpgExec(key.gpgHome(), args, bytes.NewReader(dataKey)) + err, stdout, stderr := gpgExec(key.gnuPGHome(), args, bytes.NewReader(dataKey)) if err != nil { return fmt.Errorf("%s", strings.TrimSpace(stderr.String())) } @@ -112,7 +196,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { args := []string{ "-d", } - err, stdout, stderr := gpgExec(key.gpgHome(), args, strings.NewReader(key.EncryptedKey)) + err, stdout, stderr := gpgExec(key.gnuPGHome(), args, strings.NewReader(key.EncryptedKey)) if err != nil { return nil, fmt.Errorf("%s", strings.TrimSpace(stderr.String())) } @@ -140,14 +224,14 @@ func (key MasterKey) ToMap() map[string]interface{} { return out } -// gpgHome determines the GnuPG home directory for the MasterKey, and returns +// gnuPGHome determines the GnuPG home directory for the MasterKey, and returns // its path. In order of preference: -// 1. MasterKey.homeDir +// 1. MasterKey.gnuPGHomeDir // 2. $GNUPGHOME // 3. user.Current().HomeDir/.gnupg // 4. $HOME/.gnupg -func (key *MasterKey) gpgHome() string { - if key.homeDir == "" { +func (key *MasterKey) gnuPGHome() string { + if key.gnuPGHomeDir == "" { dir := os.Getenv("GNUPGHOME") if dir == "" { usr, err := user.Current() @@ -158,15 +242,15 @@ func (key *MasterKey) gpgHome() string { } return dir } - return key.homeDir + return key.gnuPGHomeDir } // gpgExec runs the provided args with the gpgBinary, while restricting it to -// gpgHome. Stdout and stderr can be read from the returned buffers. +// gnuPGHome. Stdout and stderr can be read from the returned buffers. // When the command fails, an error is returned. -func gpgExec(gpgHome string, args []string, stdin io.Reader) (err error, stdout bytes.Buffer, stderr bytes.Buffer) { - if gpgHome != "" { - args = append([]string{"--no-default-keyring", "--homedir", gpgHome}, args...) +func gpgExec(gnuPGHome string, args []string, stdin io.Reader) (err error, stdout bytes.Buffer, stderr bytes.Buffer) { + if gnuPGHome != "" { + args = append([]string{"--no-default-keyring", "--homedir", gnuPGHome}, args...) } cmd := exec.Command(gpgBinary(), args...) @@ -177,7 +261,7 @@ func gpgExec(gpgHome string, args []string, stdin io.Reader) (err error, stdout return } -// gpgBinary returns the GNuPG binary which must be used. +// gpgBinary returns the GnuPG binary which must be used. // It allows for runtime modifications by setting the environment variable // SopsGpgExecEnv to the absolute path of the replacement binary. func gpgBinary() string { diff --git a/internal/sops/pgp/keysource_test.go b/internal/sops/pgp/keysource_test.go index cbc352b87..410e08d8e 100644 --- a/internal/sops/pgp/keysource_test.go +++ b/internal/sops/pgp/keysource_test.go @@ -18,7 +18,6 @@ package pgp import ( "bytes" - "fmt" "os" "os/user" "path/filepath" @@ -39,39 +38,161 @@ var ( func TestMasterKeyFromFingerprint(t *testing.T) { g := NewWithT(t) - key := MasterKeyFromFingerprint(mockFingerprint, "") + key := MasterKeyFromFingerprint(mockFingerprint) g.Expect(key.Fingerprint).To(Equal(mockFingerprint)) g.Expect(key.CreationDate).Should(BeTemporally("~", time.Now(), time.Second)) - g.Expect(key.homeDir).To(BeEmpty()) - key = MasterKeyFromFingerprint("B59DAF 469E8C94813 8901A 649732075E A221A7EA", "") + key = MasterKeyFromFingerprint("B59DAF 469E8C94813 8901A 649732075E A221A7EA") g.Expect(key.Fingerprint).To(Equal(mockFingerprint)) +} + +func TestNewGnuPGHome(t *testing.T) { + g := NewWithT(t) + + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).NotTo(HaveOccurred()) - key = MasterKeyFromFingerprint(mockFingerprint, "/some/path") - g.Expect(key.homeDir).To(Equal("/some/path")) + g.Expect(gnuPGHome.String()).To(BeADirectory()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.Validate()).ToNot(HaveOccurred()) } -func TestMasterKey_Encrypt(t *testing.T) { +func TestGnuPGHome_Import(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPublicKey) + gnuPGHome, err := NewGnuPGHome() g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + + b, err := os.ReadFile(mockPublicKey) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gnuPGHome.Import(b)).To(Succeed()) + + err, _, stderr := gpgExec(gnuPGHome.String(), []string{"--list-keys", mockFingerprint}, nil) + g.Expect(err).ToNot(HaveOccurred(), stderr.String()) + + b, err = os.ReadFile(mockPrivateKey) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gnuPGHome.Import(b)).To(Succeed()) + + err, _, stderr = gpgExec(gnuPGHome.String(), []string{"--list-secret-keys", mockFingerprint}, nil) + g.Expect(err).ToNot(HaveOccurred(), stderr.String()) + + g.Expect(gnuPGHome.Import([]byte("invalid armored data"))).To(HaveOccurred()) + + g.Expect(GnuPGHome("").Import(b)).To(HaveOccurred()) +} + +func TestGnuPGHome_ImportFile(t *testing.T) { + g := NewWithT(t) + + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + + g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) + g.Expect(gnuPGHome.ImportFile("invalid")).To(HaveOccurred()) +} + +func TestGnuPGHome_Validate(t *testing.T) { + t.Run("empty path", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(GnuPGHome("").Validate()).To(HaveOccurred()) + }) + + t.Run("relative path", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(GnuPGHome("../../.gnupghome").Validate()).To(HaveOccurred()) + }) + + t.Run("file path", func(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + f, err := os.CreateTemp(tmpDir, "file") + g.Expect(err).ToNot(HaveOccurred()) + defer f.Close() + + g.Expect(GnuPGHome(f.Name()).Validate()).To(HaveOccurred()) + }) + + t.Run("wrong permissions", func(t *testing.T) { + g := NewWithT(t) + + // Is created with 0755 + tmpDir := t.TempDir() + g.Expect(GnuPGHome(tmpDir).Validate()).To(HaveOccurred()) + }) + + t.Run("valid", func(t *testing.T) { + g := NewWithT(t) - key := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + gnupgHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnupgHome.String()) + }) + g.Expect(gnupgHome.Validate()).To(Succeed()) + }) +} + +func TestGnuPGHome_String(t *testing.T) { + g := NewWithT(t) + + gnuPGHome := GnuPGHome("/some/absolute/path") + g.Expect(gnuPGHome.String()).To(Equal("/some/absolute/path")) +} + +func TestGnuPGHome_ApplyToMasterKey(t *testing.T) { + g := NewWithT(t) + + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + + key := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(key) + g.Expect(key.gnuPGHomeDir).To(Equal(gnuPGHome.String())) + + gnuPGHome = "/non/existing/absolute/path/fails/validate" + gnuPGHome.ApplyToMasterKey(key) + g.Expect(key.gnuPGHomeDir).ToNot(Equal(gnuPGHome.String())) +} + +func TestMasterKey_Encrypt(t *testing.T) { + g := NewWithT(t) + + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) + + key := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(key) data := []byte("oh no, my darkest secret") g.Expect(key.Encrypt(data)).To(Succeed()) g.Expect(key.EncryptedKey).ToNot(BeEmpty()) g.Expect(key.EncryptedKey).ToNot(Equal(data)) - err, _, _ = gpgExec(gpgHome, []string{"--import", mockPrivateKey}, nil) - g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) args := []string{ "-d", } - err, stdout, stderr := gpgExec(key.gpgHome(), args, strings.NewReader(key.EncryptedKey)) + err, stdout, stderr := gpgExec(key.gnuPGHome(), args, strings.NewReader(key.EncryptedKey)) g.Expect(err).ToNot(HaveOccurred(), stderr.String()) g.Expect(stdout.Bytes()).To(Equal(data)) @@ -83,17 +204,21 @@ func TestMasterKey_Encrypt(t *testing.T) { func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPrivateKey) - g.Expect(err).NotTo(HaveOccurred()) + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) dataKey := []byte("foo") - encryptKey := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + encryptKey := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(encryptKey) g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - t.Setenv("GNUPGHOME", gpgHome) + t.Setenv("GNUPGHOME", gnuPGHome.String()) decryptKey := pgp.NewMasterKeyFromFingerprint(mockFingerprint) decryptKey.EncryptedKey = encryptKey.EncryptedKey dec, err := decryptKey.Decrypt() @@ -103,12 +228,16 @@ func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { func TestMasterKey_EncryptIfNeeded(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPrivateKey) - g.Expect(err).NotTo(HaveOccurred()) + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - key := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + key := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(key) g.Expect(key.EncryptIfNeeded([]byte("data"))).To(Succeed()) encryptedKey := key.EncryptedKey @@ -127,15 +256,18 @@ func TestMasterKey_EncryptedDataKey(t *testing.T) { func TestMasterKey_Decrypt(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPrivateKey) - g.Expect(err).NotTo(HaveOccurred()) + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) fingerprint := shortenFingerprint(mockFingerprint) data := []byte("this data is absolutely top secret") - err, stdout, stderr := gpgExec(gpgHome, []string{ + err, stdout, stderr := gpgExec(gnuPGHome.String(), []string{ "--no-default-recipient", "--yes", "--encrypt", @@ -151,7 +283,8 @@ func TestMasterKey_Decrypt(t *testing.T) { encryptedData := stdout.String() g.Expect(encryptedData).ToNot(BeEquivalentTo(data)) - key := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + key := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(key) key.EncryptedKey = encryptedData got, err := key.Decrypt() @@ -167,19 +300,24 @@ func TestMasterKey_Decrypt(t *testing.T) { func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPrivateKey) - g.Expect(err).NotTo(HaveOccurred()) + gnuPGHome, err := NewGnuPGHome() + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) dataKey := []byte("foo") - t.Setenv("GNUPGHOME", gpgHome) + t.Setenv("GNUPGHOME", gnuPGHome.String()) encryptKey := pgp.NewMasterKeyFromFingerprint(mockFingerprint) g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - decryptKey := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + decryptKey := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(decryptKey) decryptKey.EncryptedKey = encryptKey.EncryptedKey + dec, err := decryptKey.Decrypt() g.Expect(err).ToNot(HaveOccurred()) g.Expect(dec).To(Equal(dataKey)) @@ -187,12 +325,16 @@ func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - gpgHome, err := mockGpgHome(tmpDir, mockPrivateKey) + gnuPGHome, err := NewGnuPGHome() g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = os.RemoveAll(gnuPGHome.String()) + }) + g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - key := MasterKeyFromFingerprint(mockFingerprint, gpgHome) + key := MasterKeyFromFingerprint(mockFingerprint) + gnuPGHome.ApplyToMasterKey(key) data := []byte("some secret data") g.Expect(key.Encrypt(data)).To(Succeed()) @@ -206,7 +348,7 @@ func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { func TestMasterKey_NeedsRotation(t *testing.T) { g := NewWithT(t) - key := MasterKeyFromFingerprint("", "") + key := MasterKeyFromFingerprint("") g.Expect(key.NeedsRotation()).To(BeFalse()) key.CreationDate = key.CreationDate.Add(-(pgpTTL + time.Second)) @@ -216,14 +358,14 @@ func TestMasterKey_NeedsRotation(t *testing.T) { func TestMasterKey_ToString(t *testing.T) { g := NewWithT(t) - key := MasterKeyFromFingerprint(mockFingerprint, "") + key := MasterKeyFromFingerprint(mockFingerprint) g.Expect(key.ToString()).To(Equal(mockFingerprint)) } func TestMasterKey_ToMap(t *testing.T) { g := NewWithT(t) - key := MasterKeyFromFingerprint(mockFingerprint, "") + key := MasterKeyFromFingerprint(mockFingerprint) key.EncryptedKey = "data" g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ "fp": mockFingerprint, @@ -232,24 +374,24 @@ func TestMasterKey_ToMap(t *testing.T) { })) } -func TestMasterKey_gpgHome(t *testing.T) { +func TestMasterKey_gnuPGHome(t *testing.T) { g := NewWithT(t) key := &MasterKey{} usr, err := user.Current() if err == nil { - g.Expect(key.gpgHome()).To(Equal(filepath.Join(usr.HomeDir, ".gnupg"))) + g.Expect(key.gnuPGHome()).To(Equal(filepath.Join(usr.HomeDir, ".gnupg"))) } else { - g.Expect(key.gpgHome()).To(Equal(filepath.Join(os.Getenv("HOME"), ".gnupg"))) + g.Expect(key.gnuPGHome()).To(Equal(filepath.Join(os.Getenv("HOME"), ".gnupg"))) } gnupgHome := "/overwrite/home" t.Setenv("GNUPGHOME", gnupgHome) - g.Expect(key.gpgHome()).To(Equal(gnupgHome)) + g.Expect(key.gnuPGHome()).To(Equal(gnupgHome)) - key.homeDir = "/home/dir/overwrite" - g.Expect(key.gpgHome()).To(Equal(key.homeDir)) + key.gnuPGHomeDir = "/home/dir/overwrite" + g.Expect(key.gnuPGHome()).To(Equal(key.gnuPGHomeDir)) } func Test_gpgBinary(t *testing.T) { @@ -270,19 +412,3 @@ func Test_shortenFingerprint(t *testing.T) { g.Expect(shortenFingerprint(shortId)).To(Equal(shortId)) } - -func mockGpgHome(dir string, key string) (string, error) { - gpgHome := filepath.Join(dir, "gpghome") - // This is required as otherwise GPG complains about the permissions - // of the directory. - if err := os.Mkdir(gpgHome, 0700); err != nil { - return "", err - } - if key != "" { - err, _, stderr := gpgExec(gpgHome, []string{"--import", key}, nil) - if err != nil { - return "", fmt.Errorf(stderr.String()) - } - } - return gpgHome, nil -}