Write encrypt and decrypt output only once they succeed (closes #143)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
`secret encrypt` and `secret decrypt` emptied the file named by `--output` before reading any input, so a failure left it empty, and a file given as both `--input` and `--output` was emptied before it was read. They now write into a temporary file of mode 0600 next to it and rename that over it only on success, using the new `secret.WriteFileAtomicFunc`, which `secret.WriteFileAtomic` now calls. A device or pipe given as `--output`, such as `/dev/null`, is written to directly instead of being replaced by a file. Judgement call: the device and pipe case goes beyond the plan. Judgement call: a symbolic link given as `--output` is replaced, not followed. Model: opus-5-5
This commit is contained in:
+46
-44
@@ -188,36 +188,24 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
|
||||
input = file
|
||||
}
|
||||
|
||||
// Set up output writer
|
||||
output := cli.cmd.OutOrStdout()
|
||||
|
||||
if outputFile != "" {
|
||||
file, err := cli.fs.Create(outputFile)
|
||||
return cli.writeOutput(outputFile, func(output io.Writer) error {
|
||||
encryptor, err := age.Encrypt(output, recipient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
return fmt.Errorf("failed to create age encryptor: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
output = file
|
||||
}
|
||||
_, err = io.Copy(encryptor, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt data: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt the data
|
||||
encryptor, err := age.Encrypt(output, recipient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create age encryptor: %w", err)
|
||||
}
|
||||
err = encryptor.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to finalize encryption: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(encryptor, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt data: %w", err)
|
||||
}
|
||||
|
||||
err = encryptor.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to finalize encryption: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Decrypt decrypts data using an age secret key stored in a secret
|
||||
@@ -278,31 +266,45 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
|
||||
input = file
|
||||
}
|
||||
|
||||
// Set up output writer
|
||||
output := cli.cmd.OutOrStdout()
|
||||
|
||||
if outputFile != "" {
|
||||
file, err := cli.fs.Create(outputFile)
|
||||
return cli.writeOutput(outputFile, func(output io.Writer) error {
|
||||
decryptor, err := age.Decrypt(input, identity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
return fmt.Errorf("failed to create age decryptor: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(output, decryptor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// writeOutput calls write with stdout when outputFile is empty, and with
|
||||
// outputFile itself when it is a device or a pipe, such as /dev/null.
|
||||
// Otherwise write fills a new file of mode 0600 that replaces outputFile
|
||||
// only once write has succeeded, so a failure leaves outputFile as it was,
|
||||
// and outputFile may be the input file too.
|
||||
func (cli *Instance) writeOutput(
|
||||
outputFile string, write func(output io.Writer) error,
|
||||
) error {
|
||||
if outputFile == "" {
|
||||
return write(cli.cmd.OutOrStdout())
|
||||
}
|
||||
|
||||
info, err := cli.fs.Stat(outputFile)
|
||||
if err == nil && !info.Mode().IsRegular() {
|
||||
file, err := cli.fs.OpenFile(outputFile, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open output file: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
output = file
|
||||
return write(file)
|
||||
}
|
||||
|
||||
// Decrypt the data
|
||||
decryptor, err := age.Decrypt(input, identity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create age decryptor: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(output, decryptor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return secret.WriteFileAtomicFunc(cli.fs, outputFile, write)
|
||||
}
|
||||
|
||||
// isValidAgeSecretKey checks if a string is a valid age secret key by
|
||||
|
||||
Reference in New Issue
Block a user