diff options
Diffstat (limited to 'lib/crypto/gpg/gpgbin')
-rw-r--r-- | lib/crypto/gpg/gpgbin/decrypt.go | 34 | ||||
-rw-r--r-- | lib/crypto/gpg/gpgbin/encrypt.go | 35 | ||||
-rw-r--r-- | lib/crypto/gpg/gpgbin/gpgbin.go | 262 | ||||
-rw-r--r-- | lib/crypto/gpg/gpgbin/import.go | 16 | ||||
-rw-r--r-- | lib/crypto/gpg/gpgbin/sign.go | 27 | ||||
-rw-r--r-- | lib/crypto/gpg/gpgbin/verify.go | 41 |
6 files changed, 415 insertions, 0 deletions
diff --git a/lib/crypto/gpg/gpgbin/decrypt.go b/lib/crypto/gpg/gpgbin/decrypt.go new file mode 100644 index 00000000..4b8d8f2b --- /dev/null +++ b/lib/crypto/gpg/gpgbin/decrypt.go @@ -0,0 +1,34 @@ +package gpgbin + +import ( + "bytes" + "io" + "io/ioutil" + + "git.sr.ht/~rjarry/aerc/models" +) + +// Decrypt runs gpg --decrypt on the contents of r. If the packet is signed, +// the signature is also verified +func Decrypt(r io.Reader) (*models.MessageDetails, error) { + md := new(models.MessageDetails) + orig, err := ioutil.ReadAll(r) + if err != nil { + return md, err + } + args := []string{"--decrypt"} + g := newGpg(bytes.NewReader(orig), args) + err = g.cmd.Run() + if err != nil { + err = parseError(g.stderr.String()) + switch GPGErrors[err.Error()] { + case ERROR_NO_PGP_DATA_FOUND: + md.Body = bytes.NewReader(orig) + return md, nil + default: + } + } + outRdr := bytes.NewReader(g.stdout.Bytes()) + parse(outRdr, md) + return md, nil +} diff --git a/lib/crypto/gpg/gpgbin/encrypt.go b/lib/crypto/gpg/gpgbin/encrypt.go new file mode 100644 index 00000000..4cbac37b --- /dev/null +++ b/lib/crypto/gpg/gpgbin/encrypt.go @@ -0,0 +1,35 @@ +package gpgbin + +import ( + "bytes" + "io" + + "git.sr.ht/~rjarry/aerc/models" +) + +// Encrypt runs gpg --encrypt [--sign] -r [recipient]. The default is to have +// --trust-model always set +func Encrypt(r io.Reader, to []string, from string) ([]byte, error) { + //TODO probably shouldn't have --trust-model always a default + args := []string{ + "--armor", + "--trust-model", "always", + } + if from != "" { + args = append(args, "--sign", "--default-key", from) + } + for _, rcpt := range to { + args = append(args, "--recipient", rcpt) + } + args = append(args, "--encrypt", "-") + + g := newGpg(r, args) + g.cmd.Run() + outRdr := bytes.NewReader(g.stdout.Bytes()) + var md models.MessageDetails + parse(outRdr, &md) + var buf bytes.Buffer + io.Copy(&buf, md.Body) + + return buf.Bytes(), nil +} diff --git a/lib/crypto/gpg/gpgbin/gpgbin.go b/lib/crypto/gpg/gpgbin/gpgbin.go new file mode 100644 index 00000000..da046f46 --- /dev/null +++ b/lib/crypto/gpg/gpgbin/gpgbin.go @@ -0,0 +1,262 @@ +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("unknown gpg error") +} + +// 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 "" +} + +// 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" + } + } + 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) +} diff --git a/lib/crypto/gpg/gpgbin/import.go b/lib/crypto/gpg/gpgbin/import.go new file mode 100644 index 00000000..49e178b5 --- /dev/null +++ b/lib/crypto/gpg/gpgbin/import.go @@ -0,0 +1,16 @@ +package gpgbin + +import ( + "io" +) + +// Import runs gpg --import and thus imports both private and public keys +func Import(r io.Reader) error { + args := []string{"--import"} + g := newGpg(r, args) + err := g.cmd.Run() + if err != nil { + return err + } + return nil +} diff --git a/lib/crypto/gpg/gpgbin/sign.go b/lib/crypto/gpg/gpgbin/sign.go new file mode 100644 index 00000000..35ab7e7f --- /dev/null +++ b/lib/crypto/gpg/gpgbin/sign.go @@ -0,0 +1,27 @@ +package gpgbin + +import ( + "bytes" + "io" + + "git.sr.ht/~rjarry/aerc/models" +) + +// Sign creates a detached signature based on the contents of r +func Sign(r io.Reader, from string) ([]byte, string, error) { + args := []string{ + "--armor", + "--detach-sign", + "--default-key", from, + } + + g := newGpg(r, args) + g.cmd.Run() + + outRdr := bytes.NewReader(g.stdout.Bytes()) + var md models.MessageDetails + parse(outRdr, &md) + var buf bytes.Buffer + io.Copy(&buf, md.Body) + return buf.Bytes(), md.Micalg, nil +} diff --git a/lib/crypto/gpg/gpgbin/verify.go b/lib/crypto/gpg/gpgbin/verify.go new file mode 100644 index 00000000..be9f26f7 --- /dev/null +++ b/lib/crypto/gpg/gpgbin/verify.go @@ -0,0 +1,41 @@ +package gpgbin + +import ( + "bytes" + "io" + "io/ioutil" + "os" + + "git.sr.ht/~rjarry/aerc/models" +) + +// Verify runs gpg --verify. If s is not nil, then gpg interprets the +// arguments as a detached signature +func Verify(m io.Reader, s io.Reader) (*models.MessageDetails, error) { + args := []string{"--verify"} + if s != nil { + // Detached sig, save the sig to a tmp file and send msg over stdin + sig, err := ioutil.TempFile("", "sig") + if err != nil { + return nil, err + } + io.Copy(sig, s) + sig.Close() + defer os.Remove(sig.Name()) + args = append(args, sig.Name(), "-") + } + orig, err := ioutil.ReadAll(m) + if err != nil { + return nil, err + } + g := newGpg(bytes.NewReader(orig), args) + g.cmd.Run() + + out := bytes.NewReader(g.stdout.Bytes()) + md := new(models.MessageDetails) + parse(out, md) + + md.Body = bytes.NewReader(orig) + + return md, nil +} |