1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
// 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/lib/pinentry"
"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)
}
pinentry.Enable()
defer pinentry.Disable()
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
}
|