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 <tim@timculverhouse.com>
Tested-by: Koni Marti <koni.marti@gmail.com>
This commit is contained in:
Tim Culverhouse 2022-05-05 12:53:15 -05:00 committed by Robin Jarry
parent bb400c7d88
commit 32a16dcd8d
7 changed files with 107 additions and 21 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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:

View File

@ -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
}