aboutsummaryrefslogblamecommitdiffstats
path: root/lib/crypto/pgp/pgp.go
blob: 70a003a028c43f36372e254808a2abf90c65455e (plain) (tree)















































































































































































































































                                                                                                                                                                
package pgp

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path"
	"strings"
	"time"

	"git.sr.ht/~rjarry/aerc/models"
	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/emersion/go-message/mail"
	"github.com/emersion/go-pgpmail"
	"github.com/kyoh86/xdg"
	"github.com/pkg/errors"
)

type Mail struct {
	logger *log.Logger
}

var (
	Keyring openpgp.EntityList

	locked bool
)

func (m *Mail) Init(l *log.Logger) error {
	m.logger = l
	m.logger.Println("Initializing PGP keyring")
	os.MkdirAll(path.Join(xdg.DataHome(), "aerc"), 0700)

	lockpath := path.Join(xdg.DataHome(), "aerc", "keyring.lock")
	lockfile, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		// TODO: Consider connecting to main process over IPC socket
		locked = false
	} else {
		locked = true
		lockfile.Close()
	}

	keypath := path.Join(xdg.DataHome(), "aerc", "keyring.asc")
	keyfile, err := os.Open(keypath)
	if os.IsNotExist(err) {
		return nil
	} else if err != nil {
		panic(err)
	}
	defer keyfile.Close()

	Keyring, err = openpgp.ReadKeyRing(keyfile)
	if err != nil {
		panic(err)
	}
	return nil
}

func (m *Mail) Close() {
	if !locked {
		return
	}
	lockpath := path.Join(xdg.DataHome(), "aerc", "keyring.lock")
	os.Remove(lockpath)
}

func (m *Mail) getEntityByEmail(email string) (e *openpgp.Entity, err error) {
	for _, entity := range Keyring {
		ident := entity.PrimaryIdentity()
		if ident != nil && ident.UserId.Email == email {
			return entity, nil
		}
	}
	return nil, fmt.Errorf("entity not found in keyring")
}

func (m *Mail) getSignerEntityByEmail(email string) (e *openpgp.Entity, err error) {
	for _, key := range Keyring.DecryptionKeys() {
		if key.Entity == nil {
			continue
		}
		ident := key.Entity.PrimaryIdentity()
		if ident != nil && ident.UserId.Email == email {
			return key.Entity, nil
		}
	}
	return nil, fmt.Errorf("entity not found in keyring")
}

func (m *Mail) Decrypt(r io.Reader, decryptKeys openpgp.PromptFunction) (*models.MessageDetails, error) {
	md := new(models.MessageDetails)

	pgpReader, err := pgpmail.Read(r, Keyring, decryptKeys, nil)
	if err != nil {
		return nil, err
	}
	if pgpReader.MessageDetails.IsEncrypted {
		md.IsEncrypted = true
		md.DecryptedWith = pgpReader.MessageDetails.DecryptedWith.Entity.PrimaryIdentity().Name
		md.DecryptedWithKeyId = pgpReader.MessageDetails.DecryptedWith.PublicKey.KeyId
	}
	if pgpReader.MessageDetails.IsSigned {
		// we should consume the UnverifiedBody until EOF in order
		// to get the correct signature data
		data, err := ioutil.ReadAll(pgpReader.MessageDetails.UnverifiedBody)
		if err != nil {
			return nil, err
		}
		pgpReader.MessageDetails.UnverifiedBody = bytes.NewReader(data)

		md.IsSigned = true
		md.SignedBy = ""
		md.SignedByKeyId = pgpReader.MessageDetails.SignedByKeyId
		md.SignatureValidity = models.Valid
		if pgpReader.MessageDetails.SignatureError != nil {
			md.SignatureError = pgpReader.MessageDetails.SignatureError.Error()
			md.SignatureValidity = handleSignatureError(md.SignatureError)
		}
		if pgpReader.MessageDetails.SignedBy != nil {
			md.SignedBy = pgpReader.MessageDetails.SignedBy.Entity.PrimaryIdentity().Name
		}
	}
	md.Body = pgpReader.MessageDetails.UnverifiedBody
	return md, nil
}

func (m *Mail) ImportKeys(r io.Reader) error {
	keys, err := openpgp.ReadKeyRing(r)
	if err != nil {
		return err
	}
	Keyring = append(Keyring, keys...)
	if locked {
		keypath := path.Join(xdg.DataHome(), "aerc", "keyring.asc")
		keyfile, err := os.OpenFile(keypath, os.O_CREATE|os.O_APPEND, 0600)
		if err != nil {
			return err
		}
		defer keyfile.Close()

		for _, key := range keys {
			if key.PrivateKey != nil {
				err = key.SerializePrivate(keyfile, &packet.Config{})
			} else {
				err = key.Serialize(keyfile)
			}
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signerEmail string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
	var err error
	var to []*openpgp.Entity
	var signer *openpgp.Entity
	if signerEmail != "" {
		signer, err = m.getSigner(signerEmail, decryptKeys)
		if err != nil {
			return nil, err
		}
	}

	for _, rcpt := range rcpts {
		toEntity, err := m.getEntityByEmail(rcpt)
		if err != nil {
			return nil, errors.Wrap(err, "no key for "+rcpt)
		}
		to = append(to, toEntity)
	}

	cleartext, err := pgpmail.Encrypt(buf, header.Header.Header,
		to, signer, nil)
	if err != nil {
		return nil, err
	}
	return cleartext, nil
}

func (m *Mail) Sign(buf *bytes.Buffer, signerEmail string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
	var err error
	var signer *openpgp.Entity
	if signerEmail != "" {
		signer, err = m.getSigner(signerEmail, decryptKeys)
		if err != nil {
			return nil, err
		}
	}
	cleartext, err := pgpmail.Sign(buf, header.Header.Header, signer, nil)
	if err != nil {
		return nil, err
	}
	return cleartext, nil
}

func (m *Mail) getSigner(signerEmail string, decryptKeys openpgp.PromptFunction) (signer *openpgp.Entity, err error) {
	if err != nil {
		return nil, err
	}
	signer, err = m.getSignerEntityByEmail(signerEmail)
	if err != nil {
		return nil, err
	}

	key, ok := signer.SigningKey(time.Now())
	if !ok {
		return nil, fmt.Errorf("no signing key found for %s", signerEmail)
	}

	if !key.PrivateKey.Encrypted {
		return signer, nil
	}

	_, err = decryptKeys([]openpgp.Key{key}, false)
	if err != nil {
		return nil, err
	}

	return signer, nil
}

func handleSignatureError(e string) models.SignatureValidity {
	if e == "openpgp: signature made by unknown entity" {
		return models.UnknownEntity
	}
	if strings.HasPrefix(e, "pgpmail: unsupported micalg") {
		return models.UnsupportedMicalg
	}
	if strings.HasPrefix(e, "pgpmail") {
		return models.InvalidSignature
	}
	return models.UnknownValidity
}