aboutsummaryrefslogblamecommitdiffstats
path: root/lib/crypto/gpg/reader.go
blob: 07553c9734ab8d668036ecffceace699cbb2ef0e (plain) (tree)







































                                                                                                                              
                                                





















                                                                                             
                                                                                                                    



                                                                      
                                                                                                                                     






                                                                                                                                          
                                                                                                          






                                                                                      
                                                                                                                     


                                                                     
                                                                                                                                      






                                                                                                                                          
                                                                                      




                                                               
                                                                                           












                                                                                                                                                             
                                                                                                                        







                                                                             
                                                              











                                                                                                         
                                                                                                                  

                                  
                                                       


                                                       
                                     


                                 
                                                                                                               



                                           
                                                                                      









                                                                                     
// 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
}