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:
parent
bb400c7d88
commit
32a16dcd8d
7 changed files with 107 additions and 21 deletions
|
@ -2,7 +2,6 @@ package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/widgets"
|
"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, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||||
|
|
||||||
composer.SetEncrypt(!composer.Encrypt())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Provider interface {
|
||||||
Init(*log.Logger) error
|
Init(*log.Logger) error
|
||||||
Close()
|
Close()
|
||||||
GetSignerKeyId(string) (string, error)
|
GetSignerKeyId(string) (string, error)
|
||||||
|
GetKeyId(string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(s string) Provider {
|
func New(s string) Provider {
|
||||||
|
|
|
@ -55,6 +55,10 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) {
|
||||||
return gpgbin.GetPrivateKeyId(s)
|
return gpgbin.GetPrivateKeyId(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mail) GetKeyId(s string) (string, error) {
|
||||||
|
return gpgbin.GetKeyId(s)
|
||||||
|
}
|
||||||
|
|
||||||
func handleSignatureError(e string) models.SignatureValidity {
|
func handleSignatureError(e string) models.SignatureValidity {
|
||||||
if e == "gpg: missing public key" {
|
if e == "gpg: missing public key" {
|
||||||
return models.UnknownEntity
|
return models.UnknownEntity
|
||||||
|
|
|
@ -11,3 +11,13 @@ func GetPrivateKeyId(s string) (string, error) {
|
||||||
}
|
}
|
||||||
return id, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -263,6 +263,14 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) {
|
||||||
return signerEntity.PrimaryKey.KeyIdString(), nil
|
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 {
|
func handleSignatureError(e string) models.SignatureValidity {
|
||||||
if e == "openpgp: signature made by unknown entity" {
|
if e == "openpgp: signature made by unknown entity" {
|
||||||
return models.UnknownEntity
|
return models.UnknownEntity
|
||||||
|
|
|
@ -26,6 +26,7 @@ type TextInput struct {
|
||||||
scroll int
|
scroll int
|
||||||
text []rune
|
text []rune
|
||||||
change []func(ti *TextInput)
|
change []func(ti *TextInput)
|
||||||
|
focusLost []func(ti *TextInput)
|
||||||
tabcomplete func(s string) ([]string, string)
|
tabcomplete func(s string) ([]string, string)
|
||||||
completions []string
|
completions []string
|
||||||
prefix string
|
prefix string
|
||||||
|
@ -157,6 +158,9 @@ func (ti *TextInput) MouseEvent(localX int, localY int, event tcell.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ti *TextInput) Focus(focus bool) {
|
func (ti *TextInput) Focus(focus bool) {
|
||||||
|
if ti.focus && !focus {
|
||||||
|
ti.onFocusLost()
|
||||||
|
}
|
||||||
ti.focus = focus
|
ti.focus = focus
|
||||||
if focus && ti.ctx != nil {
|
if focus && ti.ctx != nil {
|
||||||
cells := runewidth.StringWidth(string(ti.text[:ti.index]))
|
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() {
|
func (ti *TextInput) updateCompletions() {
|
||||||
if ti.tabcomplete == nil {
|
if ti.tabcomplete == nil {
|
||||||
// no completer
|
// no completer
|
||||||
|
@ -304,6 +314,10 @@ func (ti *TextInput) OnChange(onChange func(ti *TextInput)) {
|
||||||
ti.change = append(ti.change, onChange)
|
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 {
|
func (ti *TextInput) Event(event tcell.Event) bool {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
|
|
|
@ -198,10 +198,23 @@ func (c *Composer) Sign() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Composer) SetEncrypt(encrypt bool) *Composer {
|
func (c *Composer) SetEncrypt(encrypt bool) *Composer {
|
||||||
|
if !encrypt {
|
||||||
c.encrypt = encrypt
|
c.encrypt = encrypt
|
||||||
c.updateCrypto()
|
c.updateCrypto()
|
||||||
return c
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Composer) Encrypt() bool {
|
func (c *Composer) Encrypt() bool {
|
||||||
return c.encrypt
|
return c.encrypt
|
||||||
|
@ -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)) {
|
func (c *Composer) OnClose(fn func(composer *Composer)) {
|
||||||
c.onClose = append(c.onClose, fn)
|
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 {
|
type reviewMessage struct {
|
||||||
composer *Composer
|
composer *Composer
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
|
@ -1094,6 +1122,7 @@ type cryptoStatus struct {
|
||||||
status *ui.Text
|
status *ui.Text
|
||||||
uiConfig *config.UIConfig
|
uiConfig *config.UIConfig
|
||||||
signKey string
|
signKey string
|
||||||
|
setEncOneShot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCryptoStatus(uiConfig *config.UIConfig) *cryptoStatus {
|
func newCryptoStatus(uiConfig *config.UIConfig) *cryptoStatus {
|
||||||
|
@ -1102,6 +1131,8 @@ func newCryptoStatus(uiConfig *config.UIConfig) *cryptoStatus {
|
||||||
title: "Security",
|
title: "Security",
|
||||||
status: ui.NewText("", defaultStyle),
|
status: ui.NewText("", defaultStyle),
|
||||||
uiConfig: uiConfig,
|
uiConfig: uiConfig,
|
||||||
|
signKey: "",
|
||||||
|
setEncOneShot: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1124,3 +1155,33 @@ func (cs *cryptoStatus) OnInvalidate(fn func(ui.Drawable)) {
|
||||||
fn(cs)
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue