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