From 57699b1fa6367a42d5877afcfdb1504e52835ed9 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 25 Apr 2022 08:30:44 -0500 Subject: feat: add gpg integration This commit adds gpg system integration. This is done through two new packages: gpgbin, which handles the system calls and parsing; and gpg which is mostly a copy of emersion/go-pgpmail with modifications to interface with package gpgbin. gpg includes tests for many cases, and by it's nature also tests package gpgbin. I separated these in case an external dependency is ever used for the gpg sys-calls/parsing (IE we mirror how go-pgpmail+openpgp currently are dependencies) Two new config options are introduced: * pgp-provider. If it is not explicitly set to "gpg", aerc will default to it's internal pgp provider * pgp-key-id: (Optionally) specify a key by short or long keyId Signed-off-by: Tim Culverhouse Acked-by: Koni Marti Acked-by: Robin Jarry --- lib/crypto/gpg/reader.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 lib/crypto/gpg/reader.go (limited to 'lib/crypto/gpg/reader.go') diff --git a/lib/crypto/gpg/reader.go b/lib/crypto/gpg/reader.go new file mode 100644 index 00000000..bf977ed4 --- /dev/null +++ b/lib/crypto/gpg/reader.go @@ -0,0 +1,165 @@ +// reader.go largerly mimics github.com/emersion/go-gpgmail, with changes made +// to interface with the gpg package in aerc + +package gpg + +import ( + "bufio" + "bytes" + "fmt" + "io" + "mime" + "strings" + + "git.sr.ht/~rjarry/aerc/lib/crypto/gpg/gpgbin" + "git.sr.ht/~rjarry/aerc/models" + "github.com/emersion/go-message/textproto" +) + +type Reader struct { + Header textproto.Header + MessageDetails *models.MessageDetails +} + +func NewReader(h textproto.Header, body io.Reader) (*Reader, error) { + t, params, err := mime.ParseMediaType(h.Get("Content-Type")) + if err != nil { + return nil, err + } + + if strings.EqualFold(t, "multipart/encrypted") && strings.EqualFold(params["protocol"], "application/pgp-encrypted") { + mr := textproto.NewMultipartReader(body, params["boundary"]) + return newEncryptedReader(h, mr) + } + if strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") { + micalg := params["micalg"] + mr := textproto.NewMultipartReader(body, params["boundary"]) + return newSignedReader(h, mr, micalg) + } + + var headerBuf bytes.Buffer + textproto.WriteHeader(&headerBuf, h) + + return &Reader{ + Header: h, + MessageDetails: &models.MessageDetails{ + Body: io.MultiReader(&headerBuf, body), + }, + }, nil +} + +func Read(r io.Reader) (*Reader, error) { + br := bufio.NewReader(r) + + h, err := textproto.ReadHeader(br) + if err != nil { + return nil, err + } + return NewReader(h, br) +} + +func newEncryptedReader(h textproto.Header, mr *textproto.MultipartReader) (*Reader, error) { + p, err := mr.NextPart() + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read first part in multipart/encrypted message: %v", err) + } + + t, _, err := mime.ParseMediaType(p.Header.Get("Content-Type")) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of first part in multipart/encrypted message: %v", err) + } + if !strings.EqualFold(t, "application/pgp-encrypted") { + return nil, fmt.Errorf("gpgmail: first part in multipart/encrypted message has type %q, not application/pgp-encrypted", t) + } + + metadata, err := textproto.ReadHeader(bufio.NewReader(p)) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to parse application/pgp-encrypted part: %v", err) + } + if s := metadata.Get("Version"); s != "1" { + return nil, fmt.Errorf("gpgmail: unsupported PGP/MIME version: %q", s) + } + + p, err = mr.NextPart() + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read second part in multipart/encrypted message: %v", err) + } + t, _, err = mime.ParseMediaType(p.Header.Get("Content-Type")) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of second part in multipart/encrypted message: %v", err) + } + if !strings.EqualFold(t, "application/octet-stream") { + return nil, fmt.Errorf("gpgmail: second part in multipart/encrypted message has type %q, not application/octet-stream", t) + } + + md, err := gpgbin.Decrypt(p) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read PGP message: %v", err) + } + + cleartext := bufio.NewReader(md.Body) + cleartextHeader, err := textproto.ReadHeader(cleartext) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read encrypted header: %v", err) + } + + t, params, err := mime.ParseMediaType(cleartextHeader.Get("Content-Type")) + if err != nil { + return nil, err + } + + if md.IsEncrypted && !md.IsSigned && strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") { + // RFC 1847 encapsulation, see RFC 3156 section 6.1 + micalg := params["micalg"] + mr := textproto.NewMultipartReader(cleartext, params["boundary"]) + mds, err := newSignedReader(cleartextHeader, mr, micalg) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read encapsulated multipart/signed message: %v", err) + } + mds.MessageDetails.IsEncrypted = md.IsEncrypted + mds.MessageDetails.DecryptedWith = md.DecryptedWith + mds.MessageDetails.DecryptedWithKeyId = md.DecryptedWithKeyId + return mds, nil + } + + var headerBuf bytes.Buffer + textproto.WriteHeader(&headerBuf, cleartextHeader) + md.Body = io.MultiReader(&headerBuf, cleartext) + + return &Reader{ + Header: h, + MessageDetails: md, + }, nil +} + +func newSignedReader(h textproto.Header, mr *textproto.MultipartReader, micalg string) (*Reader, error) { + micalg = strings.ToLower(micalg) + p, err := mr.NextPart() + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read signed part in multipart/signed message: %v", err) + } + var headerBuf bytes.Buffer + textproto.WriteHeader(&headerBuf, p.Header) + var msg bytes.Buffer + headerRdr := bytes.NewReader(headerBuf.Bytes()) + fullMsg := io.MultiReader(headerRdr, p) + io.Copy(&msg, fullMsg) + + sig, err := mr.NextPart() + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read pgp part in multipart/signed message: %v", err) + } + + md, err := gpgbin.Verify(&msg, sig) + if err != nil { + return nil, fmt.Errorf("gpgmail: failed to read PGP message: %v", err) + } + if md.Micalg != micalg && md.SignatureError == "" { + md.SignatureError = "gpg: header hash does not match actual sig hash" + } + + return &Reader{ + Header: h, + MessageDetails: md, + }, nil +} -- cgit