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:
parent
32a16dcd8d
commit
b57fceaad4
7 changed files with 174 additions and 3 deletions
32
commands/compose/attach-key.go
Normal file
32
commands/compose/attach-key.go
Normal 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
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue