aerc/lib/crypto/gpg/gpgbin/gpgbin.go
Tim Culverhouse 6a10123f4a gpg: don't send messages that failed encryption
Add error handling for messages that were unable to be encrypted.
Previously, messages that failed encryption would be sent with no
content. This patch adds error handling - when encryption fails, the
user is returned to the Review screen and instructed to check the public
keys for their recipients.

Reported-by: Moritz Poldrack <moritz@poldrack.dev>
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Moritz Poldrack <moritz@poldrack.dev>
2022-06-26 12:07:44 +02:00

288 lines
6.8 KiB
Go

package gpgbin
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/models"
"github.com/mattn/go-isatty"
)
// gpg represents a gpg command with buffers attached to stdout and stderr
type gpg struct {
cmd *exec.Cmd
stdout bytes.Buffer
stderr bytes.Buffer
}
// newGpg creates a new gpg command with buffers attached
func newGpg(stdin io.Reader, args []string) *gpg {
g := new(gpg)
g.cmd = exec.Command("gpg", "--status-fd", "1", "--batch")
g.cmd.Args = append(g.cmd.Args, args...)
g.cmd.Stdin = stdin
g.cmd.Stdout = &g.stdout
g.cmd.Stderr = &g.stderr
return g
}
// parseError parses errors returned by gpg that don't show up with a [GNUPG:]
// prefix
func parseError(s string) error {
lines := strings.Split(s, "\n")
for _, line := range lines {
line = strings.ToLower(line)
if GPGErrors[line] > 0 {
return errors.New(line)
}
}
return errors.New(strings.Join(lines, ", "))
}
// fields returns the field name from --status-fd output. See:
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS
func field(s string) string {
tokens := strings.SplitN(s, " ", 3)
if tokens[0] == "[GNUPG:]" {
return tokens[1]
}
return ""
}
// getIdentity returns the identity of the given key
func getIdentity(key uint64) string {
fpr := fmt.Sprintf("%X", key)
cmd := exec.Command("gpg", "--with-colons", "--batch", "--list-keys", fpr)
var outbuf strings.Builder
cmd.Stdout = &outbuf
cmd.Run()
out := strings.Split(outbuf.String(), "\n")
for _, line := range out {
if strings.HasPrefix(line, "uid") {
flds := strings.Split(line, ":")
return flds[9]
}
}
return ""
}
// getKeyId returns the 16 digit key id, if key exists
func getKeyId(s string, private bool) string {
cmd := exec.Command("gpg", "--with-colons", "--batch")
listArg := "--list-keys"
if private {
listArg = "--list-secret-keys"
}
cmd.Args = append(cmd.Args, listArg, s)
var outbuf strings.Builder
cmd.Stdout = &outbuf
cmd.Run()
out := strings.Split(outbuf.String(), "\n")
for _, line := range out {
if strings.HasPrefix(line, "fpr") {
flds := strings.Split(line, ":")
id := flds[9]
return id[len(id)-16:]
}
}
return ""
}
// longKeyToUint64 returns a uint64 version of the given key
func longKeyToUint64(key string) (uint64, error) {
fpr := string(key[len(key)-16:])
fprUint64, err := strconv.ParseUint(fpr, 16, 64)
if err != nil {
return 0, err
}
return fprUint64, nil
}
// parse parses the output of gpg --status-fd
func parse(r io.Reader, md *models.MessageDetails) error {
var (
logOut io.Writer
logger *log.Logger
)
if !isatty.IsTerminal(os.Stdout.Fd()) {
logOut = os.Stdout
} else {
logOut = ioutil.Discard
os.Stdout, _ = os.Open(os.DevNull)
}
logger = log.New(logOut, "", log.LstdFlags)
var err error
var msgContent []byte
var msgCollecting bool
newLine := []byte("\r\n")
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if field(line) == "PLAINTEXT_LENGTH" {
continue
}
if strings.HasPrefix(line, "[GNUPG:]") {
msgCollecting = false
logger.Println(line)
}
if msgCollecting {
msgContent = append(msgContent, scanner.Bytes()...)
msgContent = append(msgContent, newLine...)
}
switch field(line) {
case "ENC_TO":
md.IsEncrypted = true
case "DECRYPTION_KEY":
md.DecryptedWithKeyId, err = parseDecryptionKey(line)
md.DecryptedWith = getIdentity(md.DecryptedWithKeyId)
if err != nil {
return err
}
case "DECRYPTION_FAILED":
return fmt.Errorf("gpg: decryption failed")
case "PLAINTEXT":
msgCollecting = true
case "NEWSIG":
md.IsSigned = true
case "GOODSIG":
t := strings.SplitN(line, " ", 4)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
md.SignedBy = t[3]
case "BADSIG":
t := strings.SplitN(line, " ", 4)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
md.SignatureError = "gpg: invalid signature"
md.SignedBy = t[3]
case "EXPSIG":
t := strings.SplitN(line, " ", 4)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
md.SignatureError = "gpg: expired signature"
md.SignedBy = t[3]
case "EXPKEYSIG":
t := strings.SplitN(line, " ", 4)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
md.SignatureError = "gpg: signature made with expired key"
md.SignedBy = t[3]
case "REVKEYSIG":
t := strings.SplitN(line, " ", 4)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
md.SignatureError = "gpg: signature made with revoked key"
md.SignedBy = t[3]
case "ERRSIG":
t := strings.SplitN(line, " ", 9)
md.SignedByKeyId, err = longKeyToUint64(t[2])
if err != nil {
return err
}
if t[7] == "9" {
md.SignatureError = "gpg: missing public key"
}
if t[7] == "4" {
md.SignatureError = "gpg: unsupported algorithm"
}
md.SignedBy = "(unknown signer)"
case "BEGIN_ENCRYPTION":
msgCollecting = true
case "SIG_CREATED":
fields := strings.Split(line, " ")
micalg, err := strconv.Atoi(fields[4])
if err != nil {
return fmt.Errorf("gpg: micalg not found")
}
md.Micalg = micalgs[micalg]
msgCollecting = true
case "VALIDSIG":
fields := strings.Split(line, " ")
micalg, err := strconv.Atoi(fields[9])
if err != nil {
return fmt.Errorf("gpg: micalg not found")
}
md.Micalg = micalgs[micalg]
case "NODATA":
md.SignatureError = "gpg: no signature packet found"
case "FAILURE":
return fmt.Errorf(strings.TrimPrefix(line, "[GNUPG:] "))
}
}
md.Body = bytes.NewReader(msgContent)
return nil
}
// parseDecryptionKey returns primary key from DECRYPTION_KEY line
func parseDecryptionKey(l string) (uint64, error) {
key := strings.Split(l, " ")[3]
fpr := string(key[len(key)-16:])
fprUint64, err := longKeyToUint64(fpr)
if err != nil {
return 0, err
}
getIdentity(fprUint64)
return fprUint64, nil
}
type GPGError int32
const (
ERROR_NO_PGP_DATA_FOUND GPGError = iota + 1
)
var GPGErrors = map[string]GPGError{
"gpg: no valid openpgp data found.": ERROR_NO_PGP_DATA_FOUND,
}
// micalgs represent hash algorithms for signatures. These are ignored by many
// email clients, but can be used as an additional verification so are sent.
// Both gpgmail and pgpmail implementations in aerc check for matching micalgs
var micalgs = map[int]string{
1: "pgp-md5",
2: "pgp-sha1",
3: "pgp-ripemd160",
8: "pgp-sha256",
9: "pgp-sha384",
10: "pgp-sha512",
11: "pgp-sha224",
}
func logger(s string) {
var (
logOut io.Writer
logger *log.Logger
)
if !isatty.IsTerminal(os.Stdout.Fd()) {
logOut = os.Stdout
} else {
logOut = ioutil.Discard
os.Stdout, _ = os.Open(os.DevNull)
}
logger = log.New(logOut, "", log.LstdFlags)
logger.Println(s)
}