Published:
Last Edited:
n/a

Automatically Find PGP Keys in Mu4e


I have found a peculiar delight in encryption, and particularly in using pgp-based asymmetric cryptography. This is especially useful for securing email communications, but doing so requires remembering to run M-x mml-secure-sign or mml-secure-encrypt in Mail Utilities For Emacs (Mu4e) before sending the message. For this reason I have been wanting to write a quick method that automatically adds signing capabilities and encryption only the recipient has a public key. Thankfully, I did not have to do so because of Nicolas Cavigneaux’s recent post Sign Always, Encrypt When Possible where he showcases his functins for doing exactly that. It prompted me to tweak it slightly, and to add the functionality to automatically discover public keys that I have yet to import.

Cavigneaux’s post sets up an elegant arrangement: email should always be signed with your pgp key, and you should “upgrade” from there to encryption only if your correspondent can handle encrypted mail and has shared their public key with you. This is done by using Emacs’ built-in Easy Privacy Guard (epg) tooling to compare the owners of the imported public keys on the machine with the list of recipients in an email.1 If you have the public key for all recipients, the message will be automatically encrypted — otherwise it will merely be signed to prove that you are the author.

This was exactly what I had wanted, and I quickly incorporated this functionality into my own configuration. Testing it out, I felt a bit uneasy sending encrypted mail automatically. Because it is not the default, I felt that fully automated encryption made me unsure whether the mail has been encrypted or not — even when I know I have a public key. I thus made the slight modification of prompting for both encryption and signing:

(defun kudu/message-sign-encrypt-if-all-keys-available ()
    "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise."
    (if (bounga/message-all-epg-keys-available-p)
        (if (y-or-n-p "Encrypt? ")
            (mml-secure-message-sign-encrypt)
          (when (y-or-n-p "Sign? ")
            (mml-secure-message-sign)))
      (when (y-or-n-p "Sign? ")
        (mml-secure-message-sign))))

In practice I will probably always be pressing Y when prompted, but this adds a level of certainty that the code is running correctly. It reminds me of an advertising gimmick for housewives in the 1950s: when cake mix first became available, it was not appreciated because it was perceived as being “too easy” — it did not feel like baking a cake yourself. And so the recipe was tweaked to simply require an extra egg to be added. In practice this was a trivial change, but it meant that you felt more responsible for the finished work.

But there is an additional step I want to automate that Cavigneaux alluded to, but did not implement. He writes:

[T]he policy is only as good as my keyring. Encryption depends on having imported the right public keys; nothing here fetches them for me.

I have recently been having more correspondence with people using the Swiss-based email provider Protonmail. Proton provides support for the Web Key Directory (wkd) method of providing keys, where the dns settings of a domain point to a file containing a public key for a specific user. This allows the email itself to “provide” its own public key for encryption. The public key of any @protonmail.com or @pm.me address can be quickly imported in a second using gpg --locate-external-keys email@pm.me. Proton will similarly use wkd to get your public key, and so you will receive encrypted mail as well. This is still easy to set up for any other email provider, as long as you can edit the domain settings.2

Sadly, epg does not come with any options for Gnupg’s --locate-keys or --locate-external-keys flags. This might be a good addition. In the meantime, we can write a quick and dirty function to do the job for us in the background when opening any new email:

(defun kudu/message-locate-keys ()
  "Tries to find the public keys of 'bounga/message-recipients' via WKD through --locate-keys."
  (dolist (recipient (string-to-list (bounga/message-recipients)))
    (let ((recipient-email (cadr recipient))
          (proc (make-process
                 :name "gpg-locate-keys"
                 :command (list epg-gpg-program "--no-tty" "--locate-keys" (cadr recipient))
                 :connection-type 'pipe
                 :filter (lambda (proc string)
                           (process-put proc 'output
                                        (concat (or (process-get proc 'output) "") string)))
                 :sentinel (lambda (proc event)
                             (when (eq (process-status proc) 'exit)
                               (let ((output (process-get proc 'output))
                                     (email (process-get proc 'email)))
                                 (if (and output (string-match-p "imported: [1-9]" output))
                                     (message "Public key imported for %s" email))))))))
      (process-put proc 'email recipient-email))))

We use --locate-keys because the stricter --locate-external-keys will check the domain even if a key already exists in the local keyring. --locate-keys will instead instantly find local keys and finish early, while only missing keys will be checked. This is done asynchronously through make-process so that it does not cause the ui to freeze as we wait for network requests to finish. gpg seems to check very quickly, so it is not a large performance hit regardless.

We want to have imported any new keys before sending our email because Cavigneaux’s functions are run on 'message-send-hook. We therefore run kudu/message-locate-keys much earlier:

(add-hook 'mu4e-view-rendered-hook 'kudu/message-locate-keys)

This adds any correspondents’ public keys on any opened email, and so does not have to go through your entire contacts list. A downside is that it does not check for a public key when you are the person sending an unsolicited email, but it will when you get their reply.

This is not the world’s most elegant function, and if someone more familiar with epg has any comments regarding ways to improve it I will happily edit this post for improvements. Just send me a (pgp encrypted!) email. Run echo 'am9hcnhwYWJsb0B2b25hcm5kdC5zZQ' | base64 --decode | xargs gpg --locate-external-keys to import my key. If you have wkd set up, I will automatically fetch your public key. If not, this is a great opportunity to fix that!

I will also note how wonderful Emacs and epg make working with encrypted files, messages, and/or text more generally. I must admit that I used a plain-text .authinfo for an embarrassingly long time to store some of my secrets, thinking it would be bothersome to use the encrypted .authinfo.gpg for auth-sources. If you are like me, do not worry — the peace of mind from being able to write down personal information and simply leave it scattered on your filesystem is well worth just having to type your password.

Footnotes:

1

From Cavigneaux’s post, the function to get all message recipients. Returns a list formatted like (("First recipient" "first@domain.tld") ("Second recpient" "second@domain.tld") ...):

(defun bounga/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))
2

So no @gmail.com, @outlook.com, or @yahoo.com addresses. You can still use these to service your mail; wkd does not interact with any email-related dns records.

Tags: emacs technology