2022-04-25 15:30:43 +02:00
|
|
|
package pgp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2022-07-19 22:31:51 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/logging"
|
2022-04-25 15:30:43 +02:00
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
2022-05-05 19:53:16 +02:00
|
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
2022-04-25 15:30:43 +02:00
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2022-07-19 22:31:51 +02:00
|
|
|
type Mail struct{}
|
2022-04-25 15:30:43 +02:00
|
|
|
|
|
|
|
var (
|
|
|
|
Keyring openpgp.EntityList
|
|
|
|
|
|
|
|
locked bool
|
|
|
|
)
|
|
|
|
|
2022-07-19 22:31:51 +02:00
|
|
|
func (m *Mail) Init() error {
|
|
|
|
logging.Infof("Initializing PGP keyring")
|
2022-07-29 22:31:54 +02:00
|
|
|
err := os.MkdirAll(path.Join(xdg.DataHome(), "aerc"), 0o700)
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return fmt.Errorf("failed to create data directory: %w", err)
|
2022-07-29 22:31:54 +02:00
|
|
|
}
|
2022-04-25 15:30:43 +02:00
|
|
|
|
|
|
|
lockpath := path.Join(xdg.DataHome(), "aerc", "keyring.lock")
|
2022-07-31 22:16:40 +02:00
|
|
|
lockfile, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0o600)
|
2022-04-25 15:30:43 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
func (m *Mail) getSignerEntityByKeyId(id string) (*openpgp.Entity, error) {
|
|
|
|
id = strings.ToUpper(id)
|
|
|
|
for _, key := range Keyring.DecryptionKeys() {
|
|
|
|
if key.Entity == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
kId := key.Entity.PrimaryKey.KeyIdString()
|
|
|
|
if strings.Contains(kId, id) {
|
|
|
|
return key.Entity, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("entity not found in keyring")
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:43 +02:00
|
|
|
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
|
2022-08-17 16:19:45 +02:00
|
|
|
data, err := io.ReadAll(pgpReader.MessageDetails.UnverifiedBody)
|
2022-04-25 15:30:43 +02:00
|
|
|
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")
|
2022-07-31 22:16:40 +02:00
|
|
|
keyfile, err := os.OpenFile(keypath, os.O_CREATE|os.O_APPEND, 0o600)
|
2022-04-25 15:30:43 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
func (m *Mail) Encrypt(buf *bytes.Buffer, rcpts []string, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
2022-04-25 15:30:43 +02:00
|
|
|
var err error
|
|
|
|
var to []*openpgp.Entity
|
2022-04-25 15:30:44 +02:00
|
|
|
var signerEntity *openpgp.Entity
|
|
|
|
if signer != "" {
|
|
|
|
signerEntity, err = m.getSigner(signer, decryptKeys)
|
2022-04-25 15:30:43 +02:00
|
|
|
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,
|
2022-04-25 15:30:44 +02:00
|
|
|
to, signerEntity, nil)
|
2022-04-25 15:30:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return cleartext, nil
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
func (m *Mail) Sign(buf *bytes.Buffer, signer string, decryptKeys openpgp.PromptFunction, header *mail.Header) (io.WriteCloser, error) {
|
2022-04-25 15:30:43 +02:00
|
|
|
var err error
|
2022-04-25 15:30:44 +02:00
|
|
|
var signerEntity *openpgp.Entity
|
|
|
|
if signer != "" {
|
|
|
|
signerEntity, err = m.getSigner(signer, decryptKeys)
|
2022-04-25 15:30:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 15:30:44 +02:00
|
|
|
cleartext, err := pgpmail.Sign(buf, header.Header.Header, signerEntity, nil)
|
2022-04-25 15:30:43 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return cleartext, nil
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
func (m *Mail) getSigner(signer string, decryptKeys openpgp.PromptFunction) (signerEntity *openpgp.Entity, err error) {
|
|
|
|
switch strings.Contains(signer, "@") {
|
|
|
|
case true:
|
|
|
|
signerEntity, err = m.getSignerEntityByEmail(signer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
case false:
|
|
|
|
signerEntity, err = m.getSignerEntityByKeyId(signer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-04-25 15:30:43 +02:00
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
key, ok := signerEntity.SigningKey(time.Now())
|
2022-04-25 15:30:43 +02:00
|
|
|
if !ok {
|
2022-04-25 15:30:44 +02:00
|
|
|
return nil, fmt.Errorf("no signing key found for %s", signer)
|
2022-04-25 15:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if !key.PrivateKey.Encrypted {
|
2022-04-25 15:30:44 +02:00
|
|
|
return signerEntity, nil
|
2022-04-25 15:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = decryptKeys([]openpgp.Key{key}, false)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:44 +02:00
|
|
|
return signerEntity, nil
|
2022-04-25 15:30:43 +02:00
|
|
|
}
|
|
|
|
|
2022-04-29 18:19:52 +02:00
|
|
|
func (m *Mail) GetSignerKeyId(s string) (string, error) {
|
|
|
|
var err error
|
|
|
|
var signerEntity *openpgp.Entity
|
|
|
|
switch strings.Contains(s, "@") {
|
|
|
|
case true:
|
|
|
|
signerEntity, err = m.getSignerEntityByEmail(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
case false:
|
|
|
|
signerEntity, err = m.getSignerEntityByKeyId(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return signerEntity.PrimaryKey.KeyIdString(), nil
|
|
|
|
}
|
|
|
|
|
2022-05-05 19:53:15 +02:00
|
|
|
func (m *Mail) GetKeyId(s string) (string, error) {
|
|
|
|
entity, err := m.getEntityByEmail(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return entity.PrimaryKey.KeyIdString(), nil
|
|
|
|
}
|
|
|
|
|
2022-05-05 19:53:16 +02:00
|
|
|
func (m *Mail) ExportKey(k string) (io.Reader, error) {
|
|
|
|
var err error
|
|
|
|
var entity *openpgp.Entity
|
|
|
|
switch strings.Contains(k, "@") {
|
|
|
|
case true:
|
|
|
|
entity, err = m.getSignerEntityByEmail(k)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
case false:
|
|
|
|
entity, err = m.getSignerEntityByKeyId(k)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pks := bytes.NewBuffer(nil)
|
|
|
|
err = entity.Serialize(pks)
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("pgp: error exporting key: %w", err)
|
2022-05-05 19:53:16 +02:00
|
|
|
}
|
|
|
|
pka := bytes.NewBuffer(nil)
|
|
|
|
w, err := armor.Encode(pka, "PGP PUBLIC KEY BLOCK", map[string]string{})
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("pgp: error exporting key: %w", err)
|
2022-05-05 19:53:16 +02:00
|
|
|
}
|
2022-07-29 22:31:54 +02:00
|
|
|
_, err = w.Write(pks.Bytes())
|
2022-05-05 19:53:16 +02:00
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("pgp: error exporting key: %w", err)
|
2022-05-05 19:53:16 +02:00
|
|
|
}
|
|
|
|
w.Close()
|
|
|
|
return pka, nil
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:30:43 +02:00
|
|
|
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
|
|
|
|
}
|