about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2021-12-30 10:25:08 +0100
committerRobin Jarry <robin@jarry.cc>2022-01-07 13:45:34 +0100
commit69d4e3895fd15f292036320d27bbe9b83651bb78 (patch)
tree23b63b721f93e7dd8025fe6e6e519f0d5b2cf9f1
parent8813fadfe9ec33361314064a284c612e5e3fa784 (diff)
downloadaerc-69d4e3895fd15f292036320d27bbe9b83651bb78.tar.gz
pgp: PGP/MIME signing for outgoing emails
implements PGP/MIME signing with go-pgpmail. The Sign() function of
go-pgpmail requires a private (signing) key. The signing key which matches
the senders email address (from field in email header) is looked up
in aerc's copy of the keyring.

Private keys can be exported from gpg into aerc as follows:
$ gpg --export-secret-keys  >> ~/.local/share/aerc/keyring.asc

A message is signed with the ":sign" command. The sign command sets
a bool flag in the Composer struct. Using the command repeatedly will
toggle the flag.

References: https://todo.sr.ht/~rjarry/aerc/6
Signed-off-by: Koni Marti <koni.marti@gmail.com>
-rw-r--r--commands/compose/sign.go44
-rw-r--r--lib/keystore.go14
-rw-r--r--widgets/compose.go113
3 files changed, 154 insertions, 17 deletions
diff --git a/commands/compose/sign.go b/commands/compose/sign.go
new file mode 100644
index 0000000..eb985e9
--- /dev/null
+++ b/commands/compose/sign.go
@@ -0,0 +1,44 @@
+package compose
+
+import (
+	"errors"
+	"time"
+
+	"git.sr.ht/~rjarry/aerc/widgets"
+)
+
+type Sign struct{}
+
+func init() {
+	register(Sign{})
+}
+
+func (Sign) Aliases() []string {
+	return []string{"sign"}
+}
+
+func (Sign) Complete(aerc *widgets.Aerc, args []string) []string {
+	return nil
+}
+
+func (Sign) Execute(aerc *widgets.Aerc, args []string) error {
+	if len(args) != 1 {
+		return errors.New("Usage: sign")
+	}
+
+	composer, _ := aerc.SelectedTab().(*widgets.Composer)
+
+	composer.SetSign(!composer.Sign())
+
+	var statusline string
+
+	if composer.Sign() {
+		statusline = "Message will be signed."
+	} else {
+		statusline = "Message will not be signed."
+	}
+
+	aerc.PushStatus(statusline, 10*time.Second)
+
+	return nil
+}
diff --git a/lib/keystore.go b/lib/keystore.go
index df048f4..c211067 100644
--- a/lib/keystore.go
+++ b/lib/keystore.go
@@ -1,6 +1,7 @@
 package lib
 
 import (
+	"fmt"
 	"io"
 	"os"
 	"path"
@@ -52,6 +53,19 @@ func UnlockKeyring() {
 	os.Remove(lockpath)
 }
 
+func 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 ImportKeys(r io.Reader) error {
 	keys, err := openpgp.ReadKeyRing(r)
 	if err != nil {
diff --git a/widgets/compose.go b/widgets/compose.go
index 5ca0932..6b7f5cd 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/ProtonMail/go-crypto/openpgp"
 	"github.com/emersion/go-message/mail"
+	"github.com/emersion/go-pgpmail"
 	"github.com/gdamore/tcell/v2"
 	"github.com/mattn/go-runewidth"
 	"github.com/mitchellh/go-homedir"
@@ -24,6 +25,7 @@ import (
 
 	"git.sr.ht/~rjarry/aerc/completer"
 	"git.sr.ht/~rjarry/aerc/config"
+	"git.sr.ht/~rjarry/aerc/lib"
 	"git.sr.ht/~rjarry/aerc/lib/format"
 	"git.sr.ht/~rjarry/aerc/lib/templates"
 	"git.sr.ht/~rjarry/aerc/lib/ui"
@@ -49,6 +51,7 @@ type Composer struct {
 	review      *reviewMessage
 	worker      *types.Worker
 	completer   *completer.Completer
+	sign        bool
 
 	layout    HeaderLayout
 	focusable []ui.MouseableDrawableInteractive
@@ -173,6 +176,15 @@ func (c *Composer) Sent() bool {
 	return c.sent
 }
 
+func (c *Composer) SetSign(sign bool) *Composer {
+	c.sign = sign
+	return c
+}
+
+func (c *Composer) Sign() bool {
+	return c.sign
+}
+
 // Note: this does not reload the editor. You must call this before the first
 // Draw() call.
 func (c *Composer) SetContents(reader io.Reader) *Composer {
@@ -393,34 +405,74 @@ func (c *Composer) PrepareHeader() (*mail.Header, error) {
 	return c.header, nil
 }
 
+func getSenderEmail(c *Composer) (string, error) {
+	// add the from: field also to the 'recipients' list
+	if c.acctConfig.From == "" {
+		return "", errors.New("No 'From' configured for this account")
+	}
+	from, err := mail.ParseAddress(c.acctConfig.From)
+	if err != nil {
+		return "", errors.Wrap(err, "ParseAddress(config.From)")
+	}
+	return from.Address, nil
+}
+
 func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
 	if err := c.reloadEmail(); err != nil {
 		return err
 	}
 
-	if len(c.attachments) == 0 {
-		// don't create a multipart email if we only have text
-		return writeInlineBody(header, c.email, writer)
-	}
+	if c.sign {
 
-	// otherwise create a multipart email,
-	// with a multipart/alternative part for the text
-	w, err := mail.CreateWriter(writer, *header)
-	if err != nil {
-		return errors.Wrap(err, "CreateWriter")
-	}
-	defer w.Close()
+		signer, err := getSigner(c)
+		if err != nil {
+			return err
+		}
 
-	if err := writeMultipartBody(c.email, w); err != nil {
-		return errors.Wrap(err, "writeMultipartBody")
-	}
+		var signedHeader mail.Header
+		signedHeader.SetContentType("text/plain", nil)
+
+		var buf bytes.Buffer
+		var cleartext io.WriteCloser
 
-	for _, a := range c.attachments {
-		if err := writeAttachment(a, w); err != nil {
-			return errors.Wrap(err, "writeAttachment")
+		cleartext, err = pgpmail.Sign(&buf, header.Header.Header, signer, nil)
+		if err != nil {
+			return err
 		}
+
+		err = writeMsgImpl(c, &signedHeader, cleartext)
+		if err != nil {
+			return err
+		}
+		cleartext.Close()
+		io.Copy(writer, &buf)
+		return nil
+
+	} else {
+		return writeMsgImpl(c, header, writer)
 	}
+}
 
+func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
+	if len(c.attachments) == 0 {
+		// no attachements
+		return writeInlineBody(header, c.email, writer)
+	} else {
+		// with attachements
+		w, err := mail.CreateWriter(writer, *header)
+		if err != nil {
+			return errors.Wrap(err, "CreateWriter")
+		}
+		if err := writeMultipartBody(c.email, w); err != nil {
+			return errors.Wrap(err, "writeMultipartBody")
+		}
+		for _, a := range c.attachments {
+			if err := writeAttachment(a, w); err != nil {
+				return errors.Wrap(err, "writeAttachment")
+			}
+		}
+		w.Close()
+	}
 	return nil
 }
 
@@ -885,3 +937,30 @@ func (rm *reviewMessage) OnInvalidate(fn func(ui.Drawable)) {
 func (rm *reviewMessage) Draw(ctx *ui.Context) {
 	rm.grid.Draw(ctx)
 }
+
+func getSigner(c *Composer) (signer *openpgp.Entity, err error) {
+	signerEmail, err := getSenderEmail(c)
+	if err != nil {
+		return nil, err
+	}
+	signer, err = lib.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 = c.aerc.DecryptKeys([]openpgp.Key{key}, false)
+	if err != nil {
+		return nil, err
+	}
+
+	return signer, nil
+}