~rjarry/aerc

b57fceaad4bfcbd4ca3022e013b73eff72079c0b — Tim Culverhouse 3 months ago 32a16dc
pgp: add attach key command

Add compose command ("attach-key") to attach the public key associated
with the sending account. Public key is attached in ascii armor format,
with the mimetype set according to RFC 3156 ("application/pgp-keys").

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Tested-by: Koni Marti <koni.marti@gmail.com>
A commands/compose/attach-key.go => commands/compose/attach-key.go +32 -0
@@ 0,0 1,32 @@
package compose

import (
	"errors"

	"git.sr.ht/~rjarry/aerc/widgets"
)

type AttachKey struct{}

func init() {
	register(AttachKey{})
}

func (AttachKey) Aliases() []string {
	return []string{"attach-key"}
}

func (AttachKey) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func (AttachKey) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: attach-key")
	}

	composer, _ := aerc.SelectedTab().(*widgets.Composer)

	composer.SetAttachKey(!composer.AttachKey())
	return nil
}

M doc/aerc.1.scd => doc/aerc.1.scd +3 -0
@@ 383,6 383,9 @@ message list, the message in the message viewer, etc).
*attach* <path>
	Attaches the file at the given path to the email.

*attach-key*
	Attaches the public key for the configured account to the email.

*detach* [path]
	Detaches the file with the given path from the composed email. If no path is
	specified, detaches the first attachment instead.

M lib/crypto/crypto.go => lib/crypto/crypto.go +1 -0
@@ 21,6 21,7 @@ type Provider interface {
	Close()
	GetSignerKeyId(string) (string, error)
	GetKeyId(string) (string, error)
	ExportKey(string) (io.Reader, error)
}

func New(s string) Provider {

M lib/crypto/gpg/gpg.go => lib/crypto/gpg/gpg.go +4 -0
@@ 59,6 59,10 @@ func (m *Mail) GetKeyId(s string) (string, error) {
	return gpgbin.GetKeyId(s)
}

func (m *Mail) ExportKey(k string) (io.Reader, error) {
	return gpgbin.ExportPublicKey(k)
}

func handleSignatureError(e string) models.SignatureValidity {
	if e == "gpg: missing public key" {
		return models.UnknownEntity

M lib/crypto/gpg/gpgbin/keys.go => lib/crypto/gpg/gpgbin/keys.go +22 -1
@@ 1,6 1,12 @@
package gpgbin

import "fmt"
import (
	"bytes"
	"fmt"
	"io"
	"os/exec"
	"strings"
)

// GetPrivateKeyId runs gpg --list-secret-keys s
func GetPrivateKeyId(s string) (string, error) {


@@ 21,3 27,18 @@ func GetKeyId(s string) (string, error) {
	}
	return id, nil
}

// ExportPublicKey exports the public key identified by k in armor format
func ExportPublicKey(k string) (io.Reader, error) {
	cmd := exec.Command("gpg", "--export", "--armor", k)

	var outbuf bytes.Buffer
	var stderr strings.Builder
	cmd.Stdout = &outbuf
	cmd.Stderr = &stderr
	cmd.Run()
	if strings.Contains(stderr.String(), "gpg") {
		return nil, fmt.Errorf("gpg: error exporting key")
	}
	return &outbuf, nil
}

M lib/crypto/pgp/pgp.go => lib/crypto/pgp/pgp.go +34 -0
@@ 13,6 13,7 @@ import (

	"git.sr.ht/~rjarry/aerc/models"
	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/ProtonMail/go-crypto/openpgp/armor"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/emersion/go-message/mail"
	"github.com/emersion/go-pgpmail"


@@ 271,6 272,39 @@ func (m *Mail) GetKeyId(s string) (string, error) {
	return entity.PrimaryKey.KeyIdString(), nil
}

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 {
		return nil, fmt.Errorf("pgp: error exporting key: %v", err)
	}
	pka := bytes.NewBuffer(nil)
	w, err := armor.Encode(pka, "PGP PUBLIC KEY BLOCK", map[string]string{})
	if err != nil {
		return nil, fmt.Errorf("pgp: error exporting key: %v", err)
	}
	w.Write(pks.Bytes())
	if err != nil {
		return nil, fmt.Errorf("pgp: error exporting key: %v", err)
	}
	w.Close()
	return pka, nil
}

func handleSignatureError(e string) models.SignatureValidity {
	if e == "openpgp: signature made by unknown entity" {
		return models.UnknownEntity

M widgets/compose.go => widgets/compose.go +78 -2
@@ 51,6 51,7 @@ type Composer struct {
	crypto      *cryptoStatus
	sign        bool
	encrypt     bool
	attachKey   bool

	layout    HeaderLayout
	focusable []ui.MouseableDrawableInteractive


@@ 183,6 184,16 @@ func (c *Composer) Sent() bool {
	return c.sent
}

func (c *Composer) SetAttachKey(attach bool) error {
	c.attachKey = attach
	c.resetReview()
	return nil
}

func (c *Composer) AttachKey() bool {
	return c.attachKey
}

func (c *Composer) SetSign(sign bool) error {
	c.sign = sign
	err := c.updateCrypto()


@@ 581,7 592,7 @@ func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
}

func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
	if len(c.attachments) == 0 {
	if len(c.attachments) == 0 && !c.attachKey {
		// no attachements
		return writeInlineBody(header, c.email, writer)
	} else {


@@ 598,6 609,12 @@ func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
				return errors.Wrap(err, "writeAttachment")
			}
		}
		if c.attachKey {
			err := c.writeKeyAttachment(w)
			if err != nil {
				return err
			}
		}
		w.Close()
	}
	return nil


@@ 1060,6 1077,9 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
	for i := 0; i < len(composer.attachments)-1; i++ {
		spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)})
	}
	if composer.attachKey {
		spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)})
	}
	// make the last element fill remaining space
	spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)})



@@ 1085,7 1105,12 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
		grid.AddChild(ui.NewText("Attachments:",
			uiConfig.GetStyle(config.STYLE_TITLE))).At(i, 0)
		i += 1
		if len(composer.attachments) == 0 {
		if composer.attachKey {
			grid.AddChild(ui.NewText(composer.crypto.signKey+".asc",
				uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0)
			i += 1
		}
		if len(composer.attachments) == 0 && !composer.attachKey {
			grid.AddChild(ui.NewText("(none)",
				uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0)
		} else {


@@ 1185,3 1210,54 @@ func (c *Composer) checkEncryptionKeys(_ string) bool {
	c.updateCrypto()
	return true
}

func (c *Composer) writeKeyAttachment(w *mail.Writer) error {
	// Verify key exists and get keyid
	cp := c.aerc.Crypto
	var (
		err error
		s   string
	)
	if c.crypto.signKey == "" {
		if c.acctConfig.PgpKeyId != "" {
			s = c.acctConfig.PgpKeyId
		} else {
			s, err = getSenderEmail(c)
			if err != nil {
				return err
			}
		}
		c.crypto.signKey, err = cp.GetSignerKeyId(s)
		if err != nil {
			return err
		}
	}
	// Get the key in armor format
	r, err := cp.ExportKey(c.crypto.signKey)
	if err != nil {
		c.aerc.PushError(err.Error())
		return err
	}
	filename := c.crypto.signKey + ".asc"
	mimeType := "application/pgp-keys"
	params := map[string]string{
		"charset": "UTF-8",
		"name":    filename,
	}
	// set header fields
	ah := mail.AttachmentHeader{}
	ah.SetContentType(mimeType, params)
	// setting the filename auto sets the content disposition
	ah.SetFilename(filename)

	aw, err := w.CreateAttachment(ah)
	if err != nil {
		return errors.Wrap(err, "CreateKeyAttachment")
	}
	defer aw.Close()

	if _, err := io.Copy(aw, r); err != nil {
		return errors.Wrap(err, "io.Copy")
	}
	return nil
}