1package chezmoi
2
3import (
4	"os"
5	"os/exec"
6	"runtime"
7
8	"go.uber.org/multierr"
9
10	"github.com/twpayne/chezmoi/v2/internal/chezmoilog"
11)
12
13// A GPGEncryption uses gpg for encryption and decryption. See https://gnupg.org/.
14type GPGEncryption struct {
15	Command   string
16	Args      []string
17	Recipient string
18	Symmetric bool
19	Suffix    string
20}
21
22// Decrypt implements Encyrption.Decrypt.
23func (e *GPGEncryption) Decrypt(ciphertext []byte) ([]byte, error) {
24	var plaintext []byte
25	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
26		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
27		if err := os.WriteFile(ciphertextAbsPath.String(), ciphertext, 0o600); err != nil {
28			return err
29		}
30		plaintextAbsPath := tempDirAbsPath.JoinString("plaintext")
31
32		args := e.decryptArgs(plaintextAbsPath, ciphertextAbsPath)
33		if err := e.run(args); err != nil {
34			return err
35		}
36
37		var err error
38		plaintext, err = os.ReadFile(plaintextAbsPath.String())
39		return err
40	}); err != nil {
41		return nil, err
42	}
43	return plaintext, nil
44}
45
46// DecryptToFile implements Encryption.DecryptToFile.
47func (e *GPGEncryption) DecryptToFile(plaintextFilename AbsPath, ciphertext []byte) error {
48	return withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
49		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
50		if err := os.WriteFile(ciphertextAbsPath.String(), ciphertext, 0o600); err != nil {
51			return err
52		}
53		args := e.decryptArgs(plaintextFilename, ciphertextAbsPath)
54		return e.run(args)
55	})
56}
57
58// Encrypt implements Encryption.Encrypt.
59func (e *GPGEncryption) Encrypt(plaintext []byte) ([]byte, error) {
60	var ciphertext []byte
61	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
62		plaintextAbsPath := tempDirAbsPath.JoinString("plaintext")
63		if err := os.WriteFile(plaintextAbsPath.String(), plaintext, 0o600); err != nil {
64			return err
65		}
66		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
67
68		args := e.encryptArgs(plaintextAbsPath, ciphertextAbsPath)
69		if err := e.run(args); err != nil {
70			return err
71		}
72
73		var err error
74		ciphertext, err = os.ReadFile(ciphertextAbsPath.String())
75		return err
76	}); err != nil {
77		return nil, err
78	}
79	return ciphertext, nil
80}
81
82// EncryptFile implements Encryption.EncryptFile.
83func (e *GPGEncryption) EncryptFile(plaintextFilename AbsPath) ([]byte, error) {
84	var ciphertext []byte
85	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
86		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
87
88		args := e.encryptArgs(plaintextFilename, ciphertextAbsPath)
89		if err := e.run(args); err != nil {
90			return err
91		}
92
93		var err error
94		ciphertext, err = os.ReadFile(ciphertextAbsPath.String())
95		return err
96	}); err != nil {
97		return nil, err
98	}
99	return ciphertext, nil
100}
101
102// EncryptedSuffix implements Encryption.EncryptedSuffix.
103func (e *GPGEncryption) EncryptedSuffix() string {
104	return e.Suffix
105}
106
107// decryptArgs returns the arguments for decryption.
108func (e *GPGEncryption) decryptArgs(plaintextFilename, ciphertextFilename AbsPath) []string {
109	args := []string{"--output", plaintextFilename.String()}
110	args = append(args, e.Args...)
111	args = append(args, "--decrypt", ciphertextFilename.String())
112	return args
113}
114
115// encryptArgs returns the arguments for encryption.
116func (e *GPGEncryption) encryptArgs(plaintextFilename, ciphertextFilename AbsPath) []string {
117	args := []string{
118		"--armor",
119		"--output", ciphertextFilename.String(),
120	}
121	if e.Symmetric {
122		args = append(args, "--symmetric")
123	} else if e.Recipient != "" {
124		args = append(args, "--recipient", e.Recipient)
125	}
126	args = append(args, e.Args...)
127	if !e.Symmetric {
128		args = append(args, "--encrypt")
129	}
130	args = append(args, plaintextFilename.String())
131	return args
132}
133
134// run runs the command with args.
135func (e *GPGEncryption) run(args []string) error {
136	//nolint:gosec
137	cmd := exec.Command(e.Command, args...)
138	cmd.Stdin = os.Stdin
139	cmd.Stdout = os.Stdout
140	cmd.Stderr = os.Stderr
141	return chezmoilog.LogCmdRun(cmd)
142}
143
144// withPrivateTempDir creates a private temporary and calls f.
145func withPrivateTempDir(f func(tempDirAbsPath AbsPath) error) (err error) {
146	var tempDir string
147	if tempDir, err = os.MkdirTemp("", "chezmoi-encryption"); err != nil {
148		return
149	}
150	defer func() {
151		err = multierr.Append(err, os.RemoveAll(tempDir))
152	}()
153	if runtime.GOOS != "windows" {
154		if err = os.Chmod(tempDir, 0o700); err != nil {
155			return
156		}
157	}
158
159	err = f(NewAbsPath(tempDir))
160	return
161}
162