6a10123f4a
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>
287 lines
6.8 KiB
Go
287 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)
|
|
}
|