2022-04-25 15:30:44 +02:00
|
|
|
// 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
|
2022-07-29 22:31:54 +02:00
|
|
|
_ = textproto.WriteHeader(&headerBuf, h)
|
2022-04-25 15:30:44 +02:00
|
|
|
|
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read first part in multipart/encrypted message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
t, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of first part in multipart/encrypted message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to parse application/pgp-encrypted part: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read second part in multipart/encrypted message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
t, _, err = mime.ParseMediaType(p.Header.Get("Content-Type"))
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to parse Content-Type of second part in multipart/encrypted message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read PGP message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
cleartext := bufio.NewReader(md.Body)
|
|
|
|
cleartextHeader, err := textproto.ReadHeader(cleartext)
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read encrypted header: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read encapsulated multipart/signed message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
mds.MessageDetails.IsEncrypted = md.IsEncrypted
|
|
|
|
mds.MessageDetails.DecryptedWith = md.DecryptedWith
|
|
|
|
mds.MessageDetails.DecryptedWithKeyId = md.DecryptedWithKeyId
|
|
|
|
return mds, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var headerBuf bytes.Buffer
|
2022-07-29 22:31:54 +02:00
|
|
|
_ = textproto.WriteHeader(&headerBuf, cleartextHeader)
|
2022-04-25 15:30:44 +02:00
|
|
|
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 {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read signed part in multipart/signed message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
var headerBuf bytes.Buffer
|
2022-07-29 22:31:54 +02:00
|
|
|
_ = textproto.WriteHeader(&headerBuf, p.Header)
|
2022-04-25 15:30:44 +02:00
|
|
|
var msg bytes.Buffer
|
|
|
|
headerRdr := bytes.NewReader(headerBuf.Bytes())
|
|
|
|
fullMsg := io.MultiReader(headerRdr, p)
|
2022-07-29 22:31:54 +02:00
|
|
|
_, _ = io.Copy(&msg, fullMsg)
|
2022-04-25 15:30:44 +02:00
|
|
|
|
|
|
|
sig, err := mr.NextPart()
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read pgp part in multipart/signed message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
md, err := gpgbin.Verify(&msg, sig)
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("gpgmail: failed to read PGP message: %w", err)
|
2022-04-25 15:30:44 +02:00
|
|
|
}
|
|
|
|
if md.Micalg != micalg && md.SignatureError == "" {
|
|
|
|
md.SignatureError = "gpg: header hash does not match actual sig hash"
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Reader{
|
|
|
|
Header: h,
|
|
|
|
MessageDetails: md,
|
|
|
|
}, nil
|
|
|
|
}
|