// 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: %w", 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: %w", 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: %w", 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: %w", 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: %w", 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: %w", err) } cleartext := bufio.NewReader(md.Body) cleartextHeader, err := textproto.ReadHeader(cleartext) if err != nil { return nil, fmt.Errorf("gpgmail: failed to read encrypted header: %w", 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: %w", 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: %w", 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: %w", err) } md, err := gpgbin.Verify(&msg, sig) if err != nil { return nil, fmt.Errorf("gpgmail: failed to read PGP message: %w", 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 }