summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Culverhouse <tim@timculverhouse.com>2022-05-05 12:53:16 -0500
committerRobin Jarry <robin@jarry.cc>2022-05-06 11:02:55 +0200
commitb57fceaad4bfcbd4ca3022e013b73eff72079c0b (patch)
tree7e68b206ca5b5dd7d1e2a8793360a80963c0c1df
parent32a16dcd8dc488c1f360553d9d9f6d121af1b367 (diff)
downloadaerc-b57fceaad4bfcbd4ca3022e013b73eff72079c0b.zip
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>
-rw-r--r--commands/compose/attach-key.go32
-rw-r--r--doc/aerc.1.scd3
-rw-r--r--lib/crypto/crypto.go1
-rw-r--r--lib/crypto/gpg/gpg.go4
-rw-r--r--lib/crypto/gpg/gpgbin/keys.go23
-rw-r--r--lib/crypto/pgp/pgp.go34
-rw-r--r--widgets/compose.go80
7 files changed, 174 insertions, 3 deletions
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* <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.
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
+}