From 32a16dcd8dc488c1f360553d9d9f6d121af1b367 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Thu, 5 May 2022 12:53:15 -0500 Subject: [PATCH] pgp: check encryption keys before sending message Add check for public keys of all message recipients (to, cc, and bcc) before sending the message. Adds an OnFocusLost callback to header editors to facilitate a callback for checking keys whenever a new recipient is added (OnChange results in too many keyring checks). Once encryption is initially set, the callbacks are registered. If a public key is not available for any recipient, encryption is turned off. However, notably, the callbacks are still registered meaning as s soon as the user removes the recipients with missing keys, encryption is turned back on. Signed-off-by: Tim Culverhouse Tested-by: Koni Marti --- commands/compose/encrypt.go | 12 ------ lib/crypto/crypto.go | 1 + lib/crypto/gpg/gpg.go | 4 ++ lib/crypto/gpg/gpgbin/keys.go | 10 +++++ lib/crypto/pgp/pgp.go | 8 ++++ lib/ui/textinput.go | 14 +++++++ widgets/compose.go | 79 +++++++++++++++++++++++++++++++---- 7 files changed, 107 insertions(+), 21 deletions(-) diff --git a/commands/compose/encrypt.go b/commands/compose/encrypt.go index d63940b..3bd8ca4 100644 --- a/commands/compose/encrypt.go +++ b/commands/compose/encrypt.go @@ -2,7 +2,6 @@ package compose import ( "errors" - "time" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -29,16 +28,5 @@ func (Encrypt) Execute(aerc *widgets.Aerc, args []string) error { 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/crypto/crypto.go b/lib/crypto/crypto.go index cab9346..54a20e6 100644 --- a/lib/crypto/crypto.go +++ b/lib/crypto/crypto.go @@ -20,6 +20,7 @@ type Provider interface { Init(*log.Logger) error Close() GetSignerKeyId(string) (string, error) + GetKeyId(string) (string, error) } func New(s string) Provider { diff --git a/lib/crypto/gpg/gpg.go b/lib/crypto/gpg/gpg.go index 457788d..fe32468 100644 --- a/lib/crypto/gpg/gpg.go +++ b/lib/crypto/gpg/gpg.go @@ -55,6 +55,10 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) { return gpgbin.GetPrivateKeyId(s) } +func (m *Mail) GetKeyId(s string) (string, error) { + return gpgbin.GetKeyId(s) +} + func handleSignatureError(e string) models.SignatureValidity { if e == "gpg: missing public key" { return models.UnknownEntity diff --git a/lib/crypto/gpg/gpgbin/keys.go b/lib/crypto/gpg/gpgbin/keys.go index 660ce82..9c8b233 100644 --- a/lib/crypto/gpg/gpgbin/keys.go +++ b/lib/crypto/gpg/gpgbin/keys.go @@ -11,3 +11,13 @@ func GetPrivateKeyId(s string) (string, error) { } return id, nil } + +// GetKeyId runs gpg --list-keys s +func GetKeyId(s string) (string, error) { + private := false + id := getKeyId(s, private) + if id == "" { + return "", fmt.Errorf("no public key found") + } + return id, nil +} diff --git a/lib/crypto/pgp/pgp.go b/lib/crypto/pgp/pgp.go index e0c5671..f0f3f65 100644 --- a/lib/crypto/pgp/pgp.go +++ b/lib/crypto/pgp/pgp.go @@ -263,6 +263,14 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) { return signerEntity.PrimaryKey.KeyIdString(), nil } +func (m *Mail) GetKeyId(s string) (string, error) { + entity, err := m.getEntityByEmail(s) + if err != nil { + return "", err + } + return entity.PrimaryKey.KeyIdString(), nil +} + func handleSignatureError(e string) models.SignatureValidity { if e == "openpgp: signature made by unknown entity" { return models.UnknownEntity diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index aa15300..0a331dc 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -26,6 +26,7 @@ type TextInput struct { scroll int text []rune change []func(ti *TextInput) + focusLost []func(ti *TextInput) tabcomplete func(s string) ([]string, string) completions []string prefix string @@ -157,6 +158,9 @@ func (ti *TextInput) MouseEvent(localX int, localY int, event tcell.Event) { } func (ti *TextInput) Focus(focus bool) { + if ti.focus && !focus { + ti.onFocusLost() + } ti.focus = focus if focus && ti.ctx != nil { cells := runewidth.StringWidth(string(ti.text[:ti.index])) @@ -274,6 +278,12 @@ func (ti *TextInput) onChange() { } } +func (ti *TextInput) onFocusLost() { + for _, focusLost := range ti.focusLost { + focusLost(ti) + } +} + func (ti *TextInput) updateCompletions() { if ti.tabcomplete == nil { // no completer @@ -304,6 +314,10 @@ func (ti *TextInput) OnChange(onChange func(ti *TextInput)) { ti.change = append(ti.change, onChange) } +func (ti *TextInput) OnFocusLost(onFocusLost func(ti *TextInput)) { + ti.focusLost = append(ti.focusLost, onFocusLost) +} + func (ti *TextInput) Event(event tcell.Event) bool { switch event := event.(type) { case *tcell.EventKey: diff --git a/widgets/compose.go b/widgets/compose.go index 5dab429..49627fc 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -198,8 +198,21 @@ func (c *Composer) Sign() bool { } func (c *Composer) SetEncrypt(encrypt bool) *Composer { - c.encrypt = encrypt - c.updateCrypto() + if !encrypt { + c.encrypt = encrypt + c.updateCrypto() + return c + } + // Check on any attempt to encrypt, and any lost focus of "to", "cc", or + // "bcc" field. Use OnFocusLost instead of OnChange to limit keyring checks + c.encrypt = c.checkEncryptionKeys("") + if c.crypto.setEncOneShot { + // Prevent registering a lot of callbacks + c.OnFocusLost("to", c.checkEncryptionKeys) + c.OnFocusLost("cc", c.checkEncryptionKeys) + c.OnFocusLost("bcc", c.checkEncryptionKeys) + c.crypto.setEncOneShot = false + } return c } @@ -365,6 +378,15 @@ func (c *Composer) OnHeaderChange(header string, fn func(subject string)) { } } +// OnFocusLost registers an OnFocusLost callback for the specified header. +func (c *Composer) OnFocusLost(header string, fn func(input string) bool) { + if editor, ok := c.editors[strings.ToLower(header)]; ok { + editor.OnFocusLost(func() { + fn(editor.input.String()) + }) + } +} + func (c *Composer) OnClose(fn func(composer *Composer)) { c.onClose = append(c.onClose, fn) } @@ -984,6 +1006,12 @@ func (he *headerEditor) OnChange(fn func()) { }) } +func (he *headerEditor) OnFocusLost(fn func()) { + he.input.OnFocusLost(func(_ *ui.TextInput) { + fn() + }) +} + type reviewMessage struct { composer *Composer grid *ui.Grid @@ -1090,18 +1118,21 @@ func (rm *reviewMessage) Draw(ctx *ui.Context) { } type cryptoStatus struct { - title string - status *ui.Text - uiConfig *config.UIConfig - signKey string + title string + status *ui.Text + uiConfig *config.UIConfig + signKey string + setEncOneShot bool } func newCryptoStatus(uiConfig *config.UIConfig) *cryptoStatus { defaultStyle := uiConfig.GetStyle(config.STYLE_DEFAULT) return &cryptoStatus{ - title: "Security", - status: ui.NewText("", defaultStyle), - uiConfig: uiConfig, + title: "Security", + status: ui.NewText("", defaultStyle), + uiConfig: uiConfig, + signKey: "", + setEncOneShot: true, } } @@ -1124,3 +1155,33 @@ func (cs *cryptoStatus) OnInvalidate(fn func(ui.Drawable)) { fn(cs) }) } + +func (c *Composer) checkEncryptionKeys(_ string) bool { + rcpts, err := getRecipientsEmail(c) + if err != nil { + // checkEncryptionKeys gets registered as a callback and must + // explicitly call c.SetEncrypt(false) when encryption is not possible + c.SetEncrypt(false) + st := fmt.Sprintf("Cannot encrypt: %v", err) + c.aerc.statusline.PushError(st) + return false + } + var mk []string + for _, rcpt := range rcpts { + key, err := c.aerc.Crypto.GetKeyId(rcpt) + if err != nil || key == "" { + mk = append(mk, rcpt) + } + } + if len(mk) > 0 { + c.SetEncrypt(false) + st := fmt.Sprintf("Cannot encrypt, missing keys: %s", strings.Join(mk, ", ")) + c.aerc.statusline.PushError(st) + return false + } + // If callbacks were registered, encrypt will be set when user removes + // recipients with missing keys + c.encrypt = true + c.updateCrypto() + return true +}