You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aerc/lib/crypto/gpg/gpgbin/gpgbin.go

262 lines
6.2 KiB
Go

package gpgbin
import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/pinentry"
"git.sr.ht/~rjarry/aerc/models"
)
// 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", "2", "--log-file", "/dev/null", "--batch")
g.cmd.Args = append(g.cmd.Args, args...)
g.cmd.Stdin = stdin
g.cmd.Stdout = &g.stdout
g.cmd.Stderr = &g.stderr
pinentry.SetCmdEnv(g.cmd)
return g
}
// 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
err := cmd.Run()
if err != nil {
log.Errorf("gpg: failed to get identity: %v", err)
return ""
}
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
err := cmd.Run()
if err != nil {
log.Errorf("gpg: failed to get key ID: %v", err)
return ""
}
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 parseStatusFd(r io.Reader, md *models.MessageDetails) error {
var err error
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if field(line) == "PLAINTEXT_LENGTH" {
continue
}
log.Tracef(line)
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 EncryptionFailed
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 "INV_RECP":
t := strings.SplitN(line, " ", 4)
if t[2] == "10" {
return fmt.Errorf("gpg: public key of %s is not trusted", t[3])
}
case "SIG_CREATED":
fields := strings.Split(line, " ")
micalg, err := strconv.Atoi(fields[4])
if err != nil {
return MicalgNotFound
}
md.Micalg = micalgs[micalg]
case "VALIDSIG":
fields := strings.Split(line, " ")
micalg, err := strconv.Atoi(fields[9])
if err != nil {
return MicalgNotFound
}
md.Micalg = micalgs[micalg]
case "NODATA":
t := strings.SplitN(line, " ", 3)
if t[2] == "4" {
md.SignatureError = "gpg: no signature packet found"
}
if t[2] == "1" {
return NoValidOpenPgpData
}
case "FAILURE":
return fmt.Errorf("%s", strings.TrimPrefix(line, "[GNUPG:] "))
}
}
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 StatusFdParsingError int32
const (
EncryptionFailed StatusFdParsingError = iota + 1
MicalgNotFound
NoValidOpenPgpData
)
func (err StatusFdParsingError) Error() string {
switch err {
case EncryptionFailed:
return "gpg: decryption failed"
case MicalgNotFound:
return "gpg: micalg not found"
case NoValidOpenPgpData:
return "gpg: no valid OpenPGP data found"
default:
return "gpg: unknown status fd parsing error"
}
}
// 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",
}