From 61c27612d923792303ce026ca956b5ab9cc303bc Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 4 Apr 2022 16:05:47 +0200 Subject: [PATCH] sops/pgp: add GnuPGHome utility type This adds a new GnuPGHome type which can be used to create a new contained GnuPG home directory. The type is self-validating, ensuring it adheres to e.g. permission rules set out by GnuPG, and allows for importing keys from armored bytes and files. Because of this introduction, the decryptor service no longer has to write data from a Secret to a temporary file, but is instead able to directly import them into the keyring from the Secret entry's bytes. Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 43 ++--- internal/sops/keyservice/options.go | 9 +- internal/sops/keyservice/server.go | 24 +-- internal/sops/pgp/keysource.go | 114 ++++++++++-- internal/sops/pgp/keysource_test.go | 246 +++++++++++++++++++------ 5 files changed, 313 insertions(+), 123 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index a97e98426..bc8134dd7 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -22,11 +22,9 @@ import ( "encoding/base64" "fmt" "os" - "os/exec" "path/filepath" "strings" - securejoin "github.com/cyphar/filepath-securejoin" "go.mozilla.org/sops/v3" "go.mozilla.org/sops/v3/aes" "go.mozilla.org/sops/v3/cmd/sops/common" @@ -44,6 +42,7 @@ import ( kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/kustomize-controller/internal/sops/azkv" intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice" + "github.com/fluxcd/kustomize-controller/internal/sops/pgp" ) const ( @@ -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 -}