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>
This commit is contained in:
Tim Culverhouse 2022-05-05 12:53:16 -05:00 committed by Robin Jarry
parent 32a16dcd8d
commit b57fceaad4
7 changed files with 174 additions and 3 deletions

View file

@ -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
}

View file

@ -383,6 +383,9 @@ message list, the message in the message viewer, etc).
*attach* <path> *attach* <path>
Attaches the file at the given path to the email. 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] *detach* [path]
Detaches the file with the given path from the composed email. If no path is Detaches the file with the given path from the composed email. If no path is
specified, detaches the first attachment instead. specified, detaches the first attachment instead.

View file

@ -21,6 +21,7 @@ type Provider interface {
Close() Close()
GetSignerKeyId(string) (string, error) GetSignerKeyId(string) (string, error)
GetKeyId(string) (string, error) GetKeyId(string) (string, error)
ExportKey(string) (io.Reader, error)
} }
func New(s string) Provider { func New(s string) Provider {

View file

@ -59,6 +59,10 @@ func (m *Mail) GetKeyId(s string) (string, error) {
return gpgbin.GetKeyId(s) return gpgbin.GetKeyId(s)
} }
func (m *Mail) ExportKey(k string) (io.Reader, error) {
return gpgbin.ExportPublicKey(k)
}
func handleSignatureError(e string) models.SignatureValidity { func handleSignatureError(e string) models.SignatureValidity {
if e == "gpg: missing public key" { if e == "gpg: missing public key" {
return models.UnknownEntity return models.UnknownEntity

View file

@ -1,6 +1,12 @@
package gpgbin package gpgbin
import "fmt" import (
"bytes"
"fmt"
"io"
"os/exec"
"strings"
)
// GetPrivateKeyId runs gpg --list-secret-keys s // GetPrivateKeyId runs gpg --list-secret-keys s
func GetPrivateKeyId(s string) (string, error) { func GetPrivateKeyId(s string) (string, error) {
@ -21,3 +27,18 @@ func GetKeyId(s string) (string, error) {
} }
return id, nil 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
}

View file

@ -13,6 +13,7 @@ import (
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/emersion/go-pgpmail" "github.com/emersion/go-pgpmail"
@ -271,6 +272,39 @@ func (m *Mail) GetKeyId(s string) (string, error) {
return entity.PrimaryKey.KeyIdString(), nil 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 { func handleSignatureError(e string) models.SignatureValidity {
if e == "openpgp: signature made by unknown entity" { if e == "openpgp: signature made by unknown entity" {
return models.UnknownEntity return models.UnknownEntity

View file

@ -51,6 +51,7 @@ type Composer struct {
crypto *cryptoStatus crypto *cryptoStatus
sign bool sign bool
encrypt bool encrypt bool
attachKey bool
layout HeaderLayout layout HeaderLayout
focusable []ui.MouseableDrawableInteractive focusable []ui.MouseableDrawableInteractive
@ -183,6 +184,16 @@ func (c *Composer) Sent() bool {
return c.sent 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 { func (c *Composer) SetSign(sign bool) error {
c.sign = sign c.sign = sign
err := c.updateCrypto() 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 { 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 // no attachements
return writeInlineBody(header, c.email, writer) return writeInlineBody(header, c.email, writer)
} else { } else {
@ -598,6 +609,12 @@ func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
return errors.Wrap(err, "writeAttachment") return errors.Wrap(err, "writeAttachment")
} }
} }
if c.attachKey {
err := c.writeKeyAttachment(w)
if err != nil {
return err
}
}
w.Close() w.Close()
} }
return nil return nil
@ -1060,6 +1077,9 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
for i := 0; i < len(composer.attachments)-1; i++ { for i := 0; i < len(composer.attachments)-1; i++ {
spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}) 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 // make the last element fill remaining space
spec = append(spec, ui.GridSpec{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}) 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:", grid.AddChild(ui.NewText("Attachments:",
uiConfig.GetStyle(config.STYLE_TITLE))).At(i, 0) uiConfig.GetStyle(config.STYLE_TITLE))).At(i, 0)
i += 1 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)", grid.AddChild(ui.NewText("(none)",
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0) uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0)
} else { } else {
@ -1185,3 +1210,54 @@ func (c *Composer) checkEncryptionKeys(_ string) bool {
c.updateCrypto() c.updateCrypto()
return true 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
}