mirror of https://git.sr.ht/~rjarry/aerc
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.
262 lines
6.2 KiB
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",
|
|
}
|