aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2021-12-30 10:25:09 +0100
committerRobin Jarry <robin@jarry.cc>2022-01-07 13:45:34 +0100
commitb19b844a6326793f078b4a03eaf63ca96528796e (patch)
tree06fdd642c321347d0b30a5ce4def17851fbd18bf
parent69d4e3895fd15f292036320d27bbe9b83651bb78 (diff)
downloadaerc-b19b844a6326793f078b4a03eaf63ca96528796e.tar.gz
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>
-rw-r--r--commands/compose/encrypt.go44
-rw-r--r--lib/keystore.go10
-rw-r--r--widgets/compose.go84
3 files changed, 129 insertions, 9 deletions
diff --git a/commands/compose/encrypt.go b/commands/compose/encrypt.go
new file mode 100644
index 00000000..d63940b8
--- /dev/null
+++ b/commands/compose/encrypt.go
@@ -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
+}
diff --git a/lib/keystore.go b/lib/keystore.go
index c2110676..0b9d41af 100644
--- a/lib/keystore.go
+++ b/lib/keystore.go
@@ -53,6 +53,16 @@ func UnlockKeyring() {
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) {
for _, key := range Keyring.DecryptionKeys() {
if key.Entity == nil {
diff --git a/widgets/compose.go b/widgets/compose.go
index 6b7f5cd6..46d40251 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -52,6 +52,7 @@ type Composer struct {
worker *types.Worker
completer *completer.Completer
sign bool
+ encrypt bool
layout HeaderLayout
focusable []ui.MouseableDrawableInteractive
@@ -185,6 +186,15 @@ func (c *Composer) Sign() bool {
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
// Draw() call.
func (c *Composer) SetContents(reader io.Reader) *Composer {
@@ -417,27 +427,83 @@ func getSenderEmail(c *Composer) (string, error) {
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 {
if err := c.reloadEmail(); err != nil {
return err
}
- if c.sign {
-
- signer, err := getSigner(c)
- if err != nil {
- return err
- }
+ if c.sign || c.encrypt {
var signedHeader mail.Header
signedHeader.SetContentType("text/plain", nil)
var buf bytes.Buffer
var cleartext io.WriteCloser
+ var err error
- cleartext, err = pgpmail.Sign(&buf, header.Header.Header, signer, nil)
- if err != nil {
- return err
+ var signer *openpgp.Entity
+ if c.sign {
+ signer, err = getSigner(c)
+ if err != nil {
+ 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)