pgp: PGP/MIME encryption for outgoing emails

implements PGP/MIME encryption with go-pgpmail. The Encrypt() function of
go-pgpmail requires a list of public keys which are taken from the
keystore. The keystore is searched for the email addresses of all
recipients (to, cc, and bcc).
If you want to be able to read the encrypted email afterwards, add
yourself as a recipient in either to, cc, or bcc as well.

Public keys can be exported from gpg into aerc as follows:
$ gpg --export  >> ~/.local/share/aerc/keyring.asc

When composing a message, the encryption is enabled with the
":encrypt" command. This sets a bool flag in the Composer struct.
A reapted application of this command will toggle the flag.
The encrypted message can also be signed by using the ":sign"
command before or after ":encrypt".

References: https://todo.sr.ht/~rjarry/aerc/6
Signed-off-by: Koni Marti <koni.marti@gmail.com>
This commit is contained in:
Koni Marti 2021-12-30 10:25:09 +01:00 committed by Robin Jarry
parent 69d4e3895f
commit b19b844a63
3 changed files with 129 additions and 9 deletions

View File

@ -0,0 +1,44 @@
package compose
import (
"errors"
"time"
"git.sr.ht/~rjarry/aerc/widgets"
)
type Encrypt struct{}
func init() {
register(Encrypt{})
}
func (Encrypt) Aliases() []string {
return []string{"encrypt"}
}
func (Encrypt) Complete(aerc *widgets.Aerc, args []string) []string {
return nil
}
func (Encrypt) Execute(aerc *widgets.Aerc, args []string) error {
if len(args) != 1 {
return errors.New("Usage: encrypt")
}
composer, _ := aerc.SelectedTab().(*widgets.Composer)
composer.SetEncrypt(!composer.Encrypt())
var statusline string
if composer.Encrypt() {
statusline = "Message will be encrypted."
} else {
statusline = "Message will not be encrypted."
}
aerc.PushStatus(statusline, 10*time.Second)
return nil
}

View File

@ -53,6 +53,16 @@ func UnlockKeyring() {
os.Remove(lockpath) os.Remove(lockpath)
} }
func GetEntityByEmail(email string) (e *openpgp.Entity, err error) {
for _, entity := range Keyring {
ident := entity.PrimaryIdentity()
if ident != nil && ident.UserId.Email == email {
return entity, nil
}
}
return nil, fmt.Errorf("entity not found in keyring")
}
func GetSignerEntityByEmail(email string) (e *openpgp.Entity, err error) { func GetSignerEntityByEmail(email string) (e *openpgp.Entity, err error) {
for _, key := range Keyring.DecryptionKeys() { for _, key := range Keyring.DecryptionKeys() {
if key.Entity == nil { if key.Entity == nil {

View File

@ -52,6 +52,7 @@ type Composer struct {
worker *types.Worker worker *types.Worker
completer *completer.Completer completer *completer.Completer
sign bool sign bool
encrypt bool
layout HeaderLayout layout HeaderLayout
focusable []ui.MouseableDrawableInteractive focusable []ui.MouseableDrawableInteractive
@ -185,6 +186,15 @@ func (c *Composer) Sign() bool {
return c.sign return c.sign
} }
func (c *Composer) SetEncrypt(encrypt bool) *Composer {
c.encrypt = encrypt
return c
}
func (c *Composer) Encrypt() bool {
return c.encrypt
}
// 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 {
@ -417,28 +427,84 @@ func getSenderEmail(c *Composer) (string, error) {
return from.Address, nil return from.Address, nil
} }
func getRecipientsEmail(c *Composer) ([]string, error) {
h, err := c.PrepareHeader()
if err != nil {
return nil, errors.Wrap(err, "PrepareHeader")
}
// collect all 'recipients' from header (to:, cc:, bcc:)
rcpts := make(map[string]bool)
for _, key := range []string{"to", "cc", "bcc"} {
list, err := h.AddressList(key)
if err != nil {
continue
}
for _, entry := range list {
if entry != nil {
rcpts[entry.Address] = true
}
}
}
// return email addresses as string slice
results := []string{}
for email, _ := range rcpts {
results = append(results, email)
}
return results, 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 c.sign { if c.sign || c.encrypt {
signer, err := getSigner(c)
if err != nil {
return err
}
var signedHeader mail.Header var signedHeader mail.Header
signedHeader.SetContentType("text/plain", nil) signedHeader.SetContentType("text/plain", nil)
var buf bytes.Buffer var buf bytes.Buffer
var cleartext io.WriteCloser var cleartext io.WriteCloser
var err error
cleartext, err = pgpmail.Sign(&buf, header.Header.Header, signer, nil) var signer *openpgp.Entity
if c.sign {
signer, err = getSigner(c)
if err != nil { if err != nil {
return err return err
} }
} else {
signer = nil
}
if c.encrypt {
var to []*openpgp.Entity
rcpts, err := getRecipientsEmail(c)
if err != nil {
return err
}
for _, rcpt := range rcpts {
toEntity, err := lib.GetEntityByEmail(rcpt)
if err != nil {
return errors.Wrap(err, "no key for "+rcpt)
}
to = append(to, toEntity)
}
cleartext, err = pgpmail.Encrypt(&buf, header.Header.Header,
to, signer, nil)
if err != nil {
return err
}
} else {
cleartext, err = pgpmail.Sign(&buf, header.Header.Header,
signer, nil)
if err != nil {
return err
}
}
err = writeMsgImpl(c, &signedHeader, cleartext) err = writeMsgImpl(c, &signedHeader, cleartext)
if err != nil { if err != nil {