From b57fceaad4bfcbd4ca3022e013b73eff72079c0b Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Thu, 5 May 2022 12:53:16 -0500 Subject: [PATCH] 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 Tested-by: Koni Marti --- commands/compose/attach-key.go | 32 ++++++++++++++ doc/aerc.1.scd | 3 ++ lib/crypto/crypto.go | 1 + lib/crypto/gpg/gpg.go | 4 ++ lib/crypto/gpg/gpgbin/keys.go | 23 +++++++++- lib/crypto/pgp/pgp.go | 34 +++++++++++++++ widgets/compose.go | 80 +++++++++++++++++++++++++++++++++- 7 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 commands/compose/attach-key.go diff --git a/commands/compose/attach-key.go b/commands/compose/attach-key.go new file mode 100644 index 0000000..c12df44 --- /dev/null +++ b/commands/compose/attach-key.go @@ -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 +} diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index 3713917..0b33cee 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -383,6 +383,9 @@ message list, the message in the message viewer, etc). *attach* 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. diff --git a/lib/crypto/crypto.go b/lib/crypto/crypto.go index 54a20e6..3c961ad 100644 --- a/lib/crypto/crypto.go +++ b/lib/crypto/crypto.go @@ -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 { diff --git a/lib/crypto/gpg/gpg.go b/lib/crypto/gpg/gpg.go index fe32468..00125ba 100644 --- a/lib/crypto/gpg/gpg.go +++ b/lib/crypto/gpg/gpg.go @@ -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 diff --git a/lib/crypto/gpg/gpgbin/keys.go b/lib/crypto/gpg/gpgbin/keys.go index 9c8b233..bef90cf 100644 --- a/lib/crypto/gpg/gpgbin/keys.go +++ b/lib/crypto/gpg/gpgbin/keys.go @@ -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 +} diff --git a/lib/crypto/pgp/pgp.go b/lib/crypto/pgp/pgp.go index f0f3f65..4dbe37c 100644 --- a/lib/crypto/pgp/pgp.go +++ b/lib/crypto/pgp/pgp.go @@ -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 diff --git a/widgets/compose.go b/widgets/compose.go index 49627fc..8830d9d 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -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 +}