pgp: PGP/MIME signing for outgoing emails
implements PGP/MIME signing with go-pgpmail. The Sign() function of go-pgpmail requires a private (signing) key. The signing key which matches the senders email address (from field in email header) is looked up in aerc's copy of the keyring. Private keys can be exported from gpg into aerc as follows: $ gpg --export-secret-keys >> ~/.local/share/aerc/keyring.asc A message is signed with the ":sign" command. The sign command sets a bool flag in the Composer struct. Using the command repeatedly will toggle the flag. References: https://todo.sr.ht/~rjarry/aerc/6 Signed-off-by: Koni Marti <koni.marti@gmail.com>
This commit is contained in:
parent
8813fadfe9
commit
69d4e3895f
|
@ -0,0 +1,44 @@
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~rjarry/aerc/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sign struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register(Sign{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Sign) Aliases() []string {
|
||||||
|
return []string{"sign"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Sign) Complete(aerc *widgets.Aerc, args []string) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Sign) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return errors.New("Usage: sign")
|
||||||
|
}
|
||||||
|
|
||||||
|
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||||
|
|
||||||
|
composer.SetSign(!composer.Sign())
|
||||||
|
|
||||||
|
var statusline string
|
||||||
|
|
||||||
|
if composer.Sign() {
|
||||||
|
statusline = "Message will be signed."
|
||||||
|
} else {
|
||||||
|
statusline = "Message will not be signed."
|
||||||
|
}
|
||||||
|
|
||||||
|
aerc.PushStatus(statusline, 10*time.Second)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -52,6 +53,19 @@ func UnlockKeyring() {
|
||||||
os.Remove(lockpath)
|
os.Remove(lockpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSignerEntityByEmail(email string) (e *openpgp.Entity, err error) {
|
||||||
|
for _, key := range Keyring.DecryptionKeys() {
|
||||||
|
if key.Entity == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ident := key.Entity.PrimaryIdentity()
|
||||||
|
if ident != nil && ident.UserId.Email == email {
|
||||||
|
return key.Entity, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("entity not found in keyring")
|
||||||
|
}
|
||||||
|
|
||||||
func ImportKeys(r io.Reader) error {
|
func ImportKeys(r io.Reader) error {
|
||||||
keys, err := openpgp.ReadKeyRing(r)
|
keys, err := openpgp.ReadKeyRing(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-pgpmail"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/completer"
|
"git.sr.ht/~rjarry/aerc/completer"
|
||||||
"git.sr.ht/~rjarry/aerc/config"
|
"git.sr.ht/~rjarry/aerc/config"
|
||||||
|
"git.sr.ht/~rjarry/aerc/lib"
|
||||||
"git.sr.ht/~rjarry/aerc/lib/format"
|
"git.sr.ht/~rjarry/aerc/lib/format"
|
||||||
"git.sr.ht/~rjarry/aerc/lib/templates"
|
"git.sr.ht/~rjarry/aerc/lib/templates"
|
||||||
"git.sr.ht/~rjarry/aerc/lib/ui"
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
||||||
|
@ -49,6 +51,7 @@ type Composer struct {
|
||||||
review *reviewMessage
|
review *reviewMessage
|
||||||
worker *types.Worker
|
worker *types.Worker
|
||||||
completer *completer.Completer
|
completer *completer.Completer
|
||||||
|
sign bool
|
||||||
|
|
||||||
layout HeaderLayout
|
layout HeaderLayout
|
||||||
focusable []ui.MouseableDrawableInteractive
|
focusable []ui.MouseableDrawableInteractive
|
||||||
|
@ -173,6 +176,15 @@ func (c *Composer) Sent() bool {
|
||||||
return c.sent
|
return c.sent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Composer) SetSign(sign bool) *Composer {
|
||||||
|
c.sign = sign
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Composer) Sign() bool {
|
||||||
|
return c.sign
|
||||||
|
}
|
||||||
|
|
||||||
// Note: this does not reload the editor. You must call this before the first
|
// Note: this does not reload the editor. You must call this before the first
|
||||||
// Draw() call.
|
// Draw() call.
|
||||||
func (c *Composer) SetContents(reader io.Reader) *Composer {
|
func (c *Composer) SetContents(reader io.Reader) *Composer {
|
||||||
|
@ -393,34 +405,74 @@ func (c *Composer) PrepareHeader() (*mail.Header, error) {
|
||||||
return c.header, nil
|
return c.header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSenderEmail(c *Composer) (string, error) {
|
||||||
|
// add the from: field also to the 'recipients' list
|
||||||
|
if c.acctConfig.From == "" {
|
||||||
|
return "", errors.New("No 'From' configured for this account")
|
||||||
|
}
|
||||||
|
from, err := mail.ParseAddress(c.acctConfig.From)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "ParseAddress(config.From)")
|
||||||
|
}
|
||||||
|
return from.Address, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
||||||
if err := c.reloadEmail(); err != nil {
|
if err := c.reloadEmail(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.attachments) == 0 {
|
if c.sign {
|
||||||
// don't create a multipart email if we only have text
|
|
||||||
return writeInlineBody(header, c.email, writer)
|
signer, err := getSigner(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise create a multipart email,
|
var signedHeader mail.Header
|
||||||
// with a multipart/alternative part for the text
|
signedHeader.SetContentType("text/plain", nil)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var cleartext io.WriteCloser
|
||||||
|
|
||||||
|
cleartext, err = pgpmail.Sign(&buf, header.Header.Header, signer, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeMsgImpl(c, &signedHeader, cleartext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleartext.Close()
|
||||||
|
io.Copy(writer, &buf)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return writeMsgImpl(c, header, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMsgImpl(c *Composer, header *mail.Header, writer io.Writer) error {
|
||||||
|
if len(c.attachments) == 0 {
|
||||||
|
// no attachements
|
||||||
|
return writeInlineBody(header, c.email, writer)
|
||||||
|
} else {
|
||||||
|
// with attachements
|
||||||
w, err := mail.CreateWriter(writer, *header)
|
w, err := mail.CreateWriter(writer, *header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "CreateWriter")
|
return errors.Wrap(err, "CreateWriter")
|
||||||
}
|
}
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
if err := writeMultipartBody(c.email, w); err != nil {
|
if err := writeMultipartBody(c.email, w); err != nil {
|
||||||
return errors.Wrap(err, "writeMultipartBody")
|
return errors.Wrap(err, "writeMultipartBody")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range c.attachments {
|
for _, a := range c.attachments {
|
||||||
if err := writeAttachment(a, w); err != nil {
|
if err := writeAttachment(a, w); err != nil {
|
||||||
return errors.Wrap(err, "writeAttachment")
|
return errors.Wrap(err, "writeAttachment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
w.Close()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -885,3 +937,30 @@ func (rm *reviewMessage) OnInvalidate(fn func(ui.Drawable)) {
|
||||||
func (rm *reviewMessage) Draw(ctx *ui.Context) {
|
func (rm *reviewMessage) Draw(ctx *ui.Context) {
|
||||||
rm.grid.Draw(ctx)
|
rm.grid.Draw(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSigner(c *Composer) (signer *openpgp.Entity, err error) {
|
||||||
|
signerEmail, err := getSenderEmail(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signer, err = lib.GetSignerEntityByEmail(signerEmail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := signer.SigningKey(time.Now())
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no signing key found for %s", signerEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !key.PrivateKey.Encrypted {
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.aerc.DecryptKeys([]openpgp.Key{key}, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue