1package chezmoi
2
3// FIXME add builtin support for --passphrase
4// FIXME add builtin support for --symmetric
5// FIXME add builtin support for SSH keys if recommended
6
7import (
8	"bytes"
9	"io"
10	"os"
11	"os/exec"
12
13	"filippo.io/age"
14	"filippo.io/age/armor"
15	"go.uber.org/multierr"
16
17	"github.com/twpayne/chezmoi/v2/internal/chezmoilog"
18)
19
20// An AgeEncryption uses age for encryption and decryption. See
21// https://age-encryption.org.
22type AgeEncryption struct {
23	UseBuiltin      bool
24	BaseSystem      System
25	Command         string
26	Args            []string
27	Identity        AbsPath
28	Identities      []AbsPath
29	Passphrase      bool
30	Recipient       string
31	Recipients      []string
32	RecipientsFile  AbsPath
33	RecipientsFiles []AbsPath
34	Suffix          string
35	Symmetric       bool
36}
37
38// Decrypt implements Encyrption.Decrypt.
39func (e *AgeEncryption) Decrypt(ciphertext []byte) ([]byte, error) {
40	if e.UseBuiltin {
41		return e.builtinDecrypt(ciphertext)
42	}
43
44	//nolint:gosec
45	cmd := exec.Command(e.Command, append(e.decryptArgs(), e.Args...)...)
46	cmd.Stdin = bytes.NewReader(ciphertext)
47	cmd.Stderr = os.Stderr
48	return chezmoilog.LogCmdOutput(cmd)
49}
50
51// DecryptToFile implements Encryption.DecryptToFile.
52func (e *AgeEncryption) DecryptToFile(plaintextAbsPath AbsPath, ciphertext []byte) error {
53	if e.UseBuiltin {
54		plaintext, err := e.builtinDecrypt(ciphertext)
55		if err != nil {
56			return err
57		}
58		return e.BaseSystem.WriteFile(plaintextAbsPath, plaintext, 0o644)
59	}
60
61	//nolint:gosec
62	cmd := exec.Command(e.Command, append(append(e.decryptArgs(), "--output", plaintextAbsPath.String()), e.Args...)...)
63	cmd.Stdin = bytes.NewReader(ciphertext)
64	cmd.Stderr = os.Stderr
65	return chezmoilog.LogCmdRun(cmd)
66}
67
68// Encrypt implements Encryption.Encrypt.
69func (e *AgeEncryption) Encrypt(plaintext []byte) ([]byte, error) {
70	if e.UseBuiltin {
71		return e.builtinEncrypt(plaintext)
72	}
73
74	//nolint:gosec
75	cmd := exec.Command(e.Command, append(e.encryptArgs(), e.Args...)...)
76	cmd.Stdin = bytes.NewReader(plaintext)
77	cmd.Stderr = os.Stderr
78	return chezmoilog.LogCmdOutput(cmd)
79}
80
81// EncryptFile implements Encryption.EncryptFile.
82func (e *AgeEncryption) EncryptFile(plaintextAbsPath AbsPath) ([]byte, error) {
83	if e.UseBuiltin {
84		plaintext, err := e.BaseSystem.ReadFile(plaintextAbsPath)
85		if err != nil {
86			return nil, err
87		}
88		return e.builtinEncrypt(plaintext)
89	}
90
91	//nolint:gosec
92	cmd := exec.Command(e.Command, append(append(e.encryptArgs(), e.Args...), plaintextAbsPath.String())...)
93	cmd.Stderr = os.Stderr
94	return chezmoilog.LogCmdOutput(cmd)
95}
96
97// EncryptedSuffix implements Encryption.EncryptedSuffix.
98func (e *AgeEncryption) EncryptedSuffix() string {
99	return e.Suffix
100}
101
102// builtinDecrypt decrypts ciphertext using the builtin age.
103func (e *AgeEncryption) builtinDecrypt(ciphertext []byte) ([]byte, error) {
104	identities, err := e.builtinIdentities()
105	if err != nil {
106		return nil, err
107	}
108	r, err := age.Decrypt(armor.NewReader(bytes.NewReader(ciphertext)), identities...)
109	if err != nil {
110		return nil, err
111	}
112	buffer := &bytes.Buffer{}
113	if _, err = io.Copy(buffer, r); err != nil {
114		return nil, err
115	}
116	return buffer.Bytes(), err
117}
118
119// builtinEncrypt encrypts ciphertext using the builtin age.
120func (e *AgeEncryption) builtinEncrypt(plaintext []byte) ([]byte, error) {
121	recipients, err := e.builtinRecipients()
122	if err != nil {
123		return nil, err
124	}
125	output := &bytes.Buffer{}
126	armorWriter := armor.NewWriter(output)
127	writer, err := age.Encrypt(armorWriter, recipients...)
128	if err != nil {
129		return nil, err
130	}
131	if _, err := io.Copy(writer, bytes.NewReader(plaintext)); err != nil {
132		return nil, err
133	}
134	if err := writer.Close(); err != nil {
135		return nil, err
136	}
137	if err := armorWriter.Close(); err != nil {
138		return nil, err
139	}
140	return output.Bytes(), nil
141}
142
143// builtinIdentities returns the identities for decryption using the builtin
144// age.
145func (e *AgeEncryption) builtinIdentities() ([]age.Identity, error) {
146	var identities []age.Identity
147	if !e.Identity.Empty() {
148		parsedIdentities, err := parseIdentityFile(e.Identity)
149		if err != nil {
150			return nil, err
151		}
152		identities = append(identities, parsedIdentities...)
153	}
154	for _, identityAbsPath := range e.Identities {
155		parsedIdentities, err := parseIdentityFile(identityAbsPath)
156		if err != nil {
157			return nil, err
158		}
159		identities = append(identities, parsedIdentities...)
160	}
161	return identities, nil
162}
163
164// builtinRecipients returns the recipients for encryption using the builtin
165// age.
166func (e *AgeEncryption) builtinRecipients() ([]age.Recipient, error) {
167	recipients := make([]age.Recipient, 0, 1+len(e.Recipients))
168	if e.Recipient != "" {
169		parsedRecipient, err := age.ParseX25519Recipient(e.Recipient)
170		if err != nil {
171			return nil, err
172		}
173		recipients = append(recipients, parsedRecipient)
174	}
175	for _, recipient := range e.Recipients {
176		parsedRecipient, err := age.ParseX25519Recipient(recipient)
177		if err != nil {
178			return nil, err
179		}
180		recipients = append(recipients, parsedRecipient)
181	}
182	if !e.RecipientsFile.Empty() {
183		parsedRecipients, err := parseRecipientsFile(e.RecipientsFile)
184		if err != nil {
185			return nil, err
186		}
187		recipients = append(recipients, parsedRecipients...)
188	}
189	for _, recipientsFile := range e.RecipientsFiles {
190		parsedRecipients, err := parseRecipientsFile(recipientsFile)
191		if err != nil {
192			return nil, err
193		}
194		recipients = append(recipients, parsedRecipients...)
195	}
196	return recipients, nil
197}
198
199// decryptArgs returns the arguments for decryption.
200func (e *AgeEncryption) decryptArgs() []string {
201	var args []string
202	args = append(args, "--decrypt")
203	if !e.Passphrase {
204		args = append(args, e.identityArgs()...)
205	}
206	return args
207}
208
209// encryptArgs returns the arguments for encryption.
210func (e *AgeEncryption) encryptArgs() []string {
211	var args []string
212	args = append(args,
213		"--armor",
214		"--encrypt",
215	)
216	switch {
217	case e.Passphrase:
218		args = append(args, "--passphrase")
219	case e.Symmetric:
220		args = append(args, e.identityArgs()...)
221	default:
222		if e.Recipient != "" {
223			args = append(args, "--recipient", e.Recipient)
224		}
225		for _, recipient := range e.Recipients {
226			args = append(args, "--recipient", recipient)
227		}
228		if !e.RecipientsFile.Empty() {
229			args = append(args, "--recipients-file", e.RecipientsFile.String())
230		}
231		for _, recipientsFile := range e.RecipientsFiles {
232			args = append(args, "--recipients-file", recipientsFile.String())
233		}
234	}
235	return args
236}
237
238// identityArgs returns the arguments for identity.
239func (e *AgeEncryption) identityArgs() []string {
240	args := make([]string, 0, 2+2*len(e.Identities))
241	if !e.Identity.Empty() {
242		args = append(args, "--identity", e.Identity.String())
243	}
244	for _, identity := range e.Identities {
245		args = append(args, "--identity", identity.String())
246	}
247	return args
248}
249
250// parseIdentityFile parses the identities from indentityFile using the builtin
251// age.
252func parseIdentityFile(identityFile AbsPath) (identities []age.Identity, err error) {
253	var file *os.File
254	if file, err = os.Open(identityFile.String()); err != nil {
255		return
256	}
257	defer func() {
258		err = multierr.Append(err, file.Close())
259	}()
260	identities, err = age.ParseIdentities(file)
261	return
262}
263
264// parseRecipientFile parses the recipients from recipientFile using the builtin
265// age.
266func parseRecipientsFile(recipientsFile AbsPath) (recipients []age.Recipient, err error) {
267	var file *os.File
268	if file, err = os.Open(recipientsFile.String()); err != nil {
269		return
270	}
271	defer func() {
272		err = multierr.Append(err, file.Close())
273	}()
274	recipients, err = age.ParseRecipients(file)
275	return
276}
277