summaryrefslogtreecommitdiff
path: root/lisp/gnus/mml-sec.el
diff options
context:
space:
mode:
authorJens Lechtenboerger <jens.lechtenboerger@fsfe.org>2016-01-03 01:10:34 +0000
committerKatsumi Yamaoka <yamaoka@jpl.org>2016-01-03 01:10:34 +0000
commit5213ded9aab68d83c306aa2f4880c8a1abd3608c (patch)
tree67bf83af8552079df3a2f559174a02e58fdd739e /lisp/gnus/mml-sec.el
parent43662a240b682de94299e797452ba56d01a04883 (diff)
downloademacs-5213ded9aab68d83c306aa2f4880c8a1abd3608c.tar.gz
Refactor mml-smime.el, mml1991.el, mml2015.el
(Maybe this is the last merge from Gnus git to Emacs git) Cf. discussion on ding mailing list, messages in <http://thread.gmane.org/gmane.emacs.gnus.general/86228>. Common code from the three files mml-smime.el, mml1991.el, and mml2015.el is moved to mml-sec.el. Auxiliary functions are added to gnus-util.el. The code is supported by test cases with necessary test keys. Documentation in message.texi is updated. * doc/misc/message.texi (Security, Using S/MIME): Update for refactoring mml-smime.el, mml1991.el, mml2015.el. (Using OpenPGP): Rename from "Using PGP/MIME"; update contents. (Passphrase caching, Encrypt-to-self, Bcc Warning): New sections. * lisp/gnus/gnus-util.el (gnus-test-list, gnus-subsetp, gnus-setdiff): New functions. * lisp/gnus/mml-sec.el: Require gnus-util and epg. (epa--select-keys): Autoload. (mml-signencrypt-style-alist, mml-secure-cache-passphrase): Doc fix. (mml-secure-openpgp-signers): New user option; make mml1991-signers and mml2015-signers obsolete aliases to it. (mml-secure-smime-signers): New user option; make mml-smime-signers an obsolete alias to it. (mml-secure-openpgp-encrypt-to-self): New user option; make mml1991-encrypt-to-self and mml2015-encrypt-to-self obsolete aliases to it. (mml-secure-smime-encrypt-to-self): New user option; make mml-smime-encrypt-to-self an obsolete alias to it. (mml-secure-openpgp-sign-with-sender): New user option; make mml2015-sign-with-sender an obsolete alias to it. (mml-secure-smime-sign-with-sender): New user option; make mml-smime-sign-with-sender an obsolete alias to it. (mml-secure-openpgp-always-trust): New user option; make mml2015-always-trust an obsolete alias to it. (mml-secure-fail-when-key-problem, mml-secure-key-preferences): New user options. (mml-secure-cust-usage-lookup, mml-secure-cust-fpr-lookup) (mml-secure-cust-record-keys, mml-secure-cust-remove-keys) (mml-secure-add-secret-key-id, mml-secure-clear-secret-key-id-list) (mml-secure-cache-passphrase-p, mml-secure-cache-expiry-interval) (mml-secure-passphrase-callback, mml-secure-check-user-id) (mml-secure-secret-key-exists-p, mml-secure-check-sub-key) (mml-secure-find-usable-keys, mml-secure-select-preferred-keys) (mml-secure-fingerprint, mml-secure-filter-keys) (mml-secure-normalize-cust-name, mml-secure-select-keys) (mml-secure-select-keys-1, mml-secure-signer-names, mml-secure-signers) (mml-secure-self-recipients, mml-secure-recipients) (mml-secure-epg-encrypt, mml-secure-epg-sign): New functions. * lisp/gnus/mml-smime.el: Require epg; refactor declaration and autoloading of epg functions. (mml-smime-use): Doc fix. (mml-smime-cache-passphrase, mml-smime-passphrase-cache-expiry): Obsolete. (mml-smime-get-dns-cert, mml-smime-get-ldap-cert): Use format instead of gnus-format-message. (mml-smime-epg-secret-key-id-list): Remove variable. (mml-smime-epg-passphrase-callback, mml-smime-epg-find-usable-key) (mml-smime-epg-find-usable-secret-key): Remove functions. (mml-smime-epg-sign, mml-smime-epg-encrypt): Refactor. * lisp/gnus/mml1991.el (mml1991-cache-passphrase) (mml1991-passphrase-cache-expiry): Obsolete. (mml1991-epg-secret-key-id-list): Remove variable. (mml1991-epg-passphrase-callback, mml1991-epg-find-usable-key) (mml1991-epg-find-usable-secret-key): Remove functions. (mml1991-epg-sign, mml1991-epg-encrypt): Refactor. * lisp/gnus/mml2015.el (mml2015-cache-passphrase) (mml2015-passphrase-cache-expiry): Obsolete. (mml2015-epg-secret-key-id-list): Remove variable. (mml2015-epg-passphrase-callback, mml2015-epg-check-user-id) (mml2015-epg-check-sub-key, mml2015-epg-find-usable-key) (mml2015-epg-find-usable-secret-key): Remove functions. (mml2015-epg-decrypt, mml2015-epg-clear-decrypt, mml2015-epg-sign) (mml2015-epg-encrypt): Refactor.
Diffstat (limited to 'lisp/gnus/mml-sec.el')
-rw-r--r--lisp/gnus/mml-sec.el579
1 files changed, 576 insertions, 3 deletions
diff --git a/lisp/gnus/mml-sec.el b/lisp/gnus/mml-sec.el
index e4c90956788..0a5f472079d 100644
--- a/lisp/gnus/mml-sec.el
+++ b/lisp/gnus/mml-sec.el
@@ -25,7 +25,9 @@
(eval-when-compile (require 'cl))
-(autoload 'gnus-subsetp "gnus-util")
+(require 'gnus-util)
+(require 'epg)
+
(autoload 'mail-strip-quoted-names "mail-utils")
(autoload 'mml2015-sign "mml2015")
(autoload 'mml2015-encrypt "mml2015")
@@ -40,6 +42,7 @@
(autoload 'mml-smime-encrypt-query "mml-smime")
(autoload 'mml-smime-verify "mml-smime")
(autoload 'mml-smime-verify-test "mml-smime")
+(autoload 'epa--select-keys "epa")
(defvar mml-sign-alist
'(("smime" mml-smime-sign-buffer mml-smime-sign-query)
@@ -91,7 +94,7 @@ signs and encrypt the message in one step.
Note that the output generated by using a `combined' mode is NOT
understood by all PGP implementations, in particular PGP version
-2 does not support it! See Info node `(message)Security' for
+2 does not support it! See Info node `(message) Security' for
details."
:version "22.1"
:group 'message
@@ -111,7 +114,9 @@ details."
(if (boundp 'password-cache)
password-cache
t)
- "If t, cache passphrase."
+ "If t, cache OpenPGP or S/MIME passphrases inside Emacs.
+Passphrase caching in Emacs is NOT recommended. Use gpg-agent instead.
+See Info node `(message) Security'."
:group 'message
:type 'boolean)
@@ -125,6 +130,21 @@ Whether the passphrase is cached at all is controlled by
:group 'message
:type 'integer)
+(defcustom mml-secure-safe-bcc-list nil
+ "List of e-mail addresses that are safe to use in Bcc headers.
+EasyPG encrypts e-mails to Bcc addresses, and the encrypted e-mail
+by default identifies the used encryption keys, giving away the
+Bcc'ed identities. Clearly, this contradicts the original goal of
+*blind* copies.
+For an academic paper explaining the problem, see URL
+`http://crypto.stanford.edu/portia/papers/bb-bcc.pdf'.
+Use this variable to specify e-mail addresses whose owners do not
+mind if they are identifiable as recipients. This may be useful if
+you use Bcc headers to encrypt e-mails to yourself."
+ :version "25.1"
+ :group 'message
+ :type '(repeat string))
+
;;; Configuration/helper functions
(defun mml-signencrypt-style (method &optional style)
@@ -275,6 +295,36 @@ Use METHOD if given. Else use `mml-secure-method' or
(interactive)
(mml-secure-part "smime"))
+(defun mml-secure-is-encrypted-p ()
+ "Check whether secure encrypt tag is present."
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward
+ (concat "^" (regexp-quote mail-header-separator) "\n"
+ "<#secure[^>]+encrypt")
+ nil t)))
+
+(defun mml-secure-bcc-is-safe ()
+ "Check whether usage of Bcc is safe (or absent).
+Bcc usage is safe in two cases: first, if the current message does
+not contain an MML secure encrypt tag;
+second, if the Bcc addresses are a subset of `mml-secure-safe-bcc-list'.
+In all other cases, ask the user whether Bcc usage is safe.
+Raise error if user answers no.
+Note that this function does not produce a meaningful return value:
+either an error is raised or not."
+ (when (mml-secure-is-encrypted-p)
+ (let ((bcc (mail-strip-quoted-names (message-fetch-field "bcc"))))
+ (when bcc
+ (let ((bcc-list (mapcar #'cadr
+ (mail-extract-address-components bcc t))))
+ (unless (gnus-subsetp bcc-list mml-secure-safe-bcc-list)
+ (unless (yes-or-no-p "Message for encryption contains Bcc header.\
+ This may give away all Bcc'ed identities to all recipients.\
+ Are you sure that this is safe?\
+ (Customize `mml-secure-safe-bcc-list' to avoid this warning.) ")
+ (error "Aborted"))))))))
+
;; defuns that add the proper <#secure ...> tag to the top of the message body
(defun mml-secure-message (method &optional modesym)
(let ((mode (prin1-to-string modesym))
@@ -380,6 +430,529 @@ If called with a prefix argument, only encrypt (do NOT sign)."
(interactive "P")
(mml-secure-message "pgpauto" (if dontsign 'encrypt 'signencrypt)))
+;;; Common functionality for mml1991.el, mml2015.el, mml-smime.el
+
+(define-obsolete-variable-alias 'mml1991-signers 'mml-secure-openpgp-signers)
+(define-obsolete-variable-alias 'mml2015-signers 'mml-secure-openpgp-signers)
+(defcustom mml-secure-openpgp-signers nil
+ "A list of your own key ID(s) which will be used to sign OpenPGP messages.
+If set, it is added to the setting of `mml-secure-openpgp-sign-with-sender'."
+ :group 'mime-security
+ :type '(repeat (string :tag "Key ID")))
+
+(define-obsolete-variable-alias 'mml-smime-signers 'mml-secure-smime-signers)
+(defcustom mml-secure-smime-signers nil
+ "A list of your own key ID(s) which will be used to sign S/MIME messages.
+If set, it is added to the setting of `mml-secure-smime-sign-with-sender'."
+ :group 'mime-security
+ :type '(repeat (string :tag "Key ID")))
+
+(define-obsolete-variable-alias
+ 'mml1991-encrypt-to-self 'mml-secure-openpgp-encrypt-to-self)
+(define-obsolete-variable-alias
+ 'mml2015-encrypt-to-self 'mml-secure-openpgp-encrypt-to-self)
+(defcustom mml-secure-openpgp-encrypt-to-self nil
+ "List of own key ID(s) or t; determines additional recipients with OpenPGP.
+If t, also encrypt to key for message sender; if list, encrypt to those keys.
+With this variable, you can ensure that you can decrypt your own messages.
+Alternatives to this variable include Bcc'ing the message to yourself or
+using the encrypt-to or hidden-encrypt-to option in gpg.conf (see man gpg(1)).
+Note that this variable and the encrypt-to option give away your identity
+for *every* encryption without warning, which is not what you want if you are
+using, e.g., remailers.
+Also, use of Bcc gives away your identity for *every* encryption without
+warning, which is a bug, see:
+https://debbugs.gnu.org/cgi/bugreport.cgi?bug=18718"
+ :group 'mime-security
+ :type '(choice (const :tag "None" nil)
+ (const :tag "From address" t)
+ (repeat (string :tag "Key ID"))))
+
+(define-obsolete-variable-alias
+ 'mml-smime-encrypt-to-self 'mml-secure-smime-encrypt-to-self)
+(defcustom mml-secure-smime-encrypt-to-self nil
+ "List of own key ID(s) or t; determines additional recipients with S/MIME.
+If t, also encrypt to key for message sender; if list, encrypt to those keys.
+With this variable, you can ensure that you can decrypt your own messages.
+Alternatives to this variable include Bcc'ing the message to yourself or
+using the encrypt-to option in gpgsm.conf (see man gpgsm(1)).
+Note that this variable and the encrypt-to option give away your identity
+for *every* encryption without warning, which is not what you want if you are
+using, e.g., remailers.
+Also, use of Bcc gives away your identity for *every* encryption without
+warning, which is a bug, see:
+https://debbugs.gnu.org/cgi/bugreport.cgi?bug=18718"
+ :group 'mime-security
+ :type '(choice (const :tag "None" nil)
+ (const :tag "From address" t)
+ (repeat (string :tag "Key ID"))))
+
+(define-obsolete-variable-alias
+ 'mml2015-sign-with-sender 'mml-secure-openpgp-sign-with-sender)
+;mml1991-sign-with-sender did never exist.
+(defcustom mml-secure-openpgp-sign-with-sender nil
+ "If t, use message sender to find an OpenPGP key to sign with."
+ :group 'mime-security
+ :type 'boolean)
+
+(define-obsolete-variable-alias
+ 'mml-smime-sign-with-sender 'mml-secure-smime-sign-with-sender)
+(defcustom mml-secure-smime-sign-with-sender nil
+ "If t, use message sender to find an S/MIME key to sign with."
+ :group 'mime-security
+ :type 'boolean)
+
+(define-obsolete-variable-alias
+ 'mml2015-always-trust 'mml-secure-openpgp-always-trust)
+;mml1991-always-trust did never exist.
+(defcustom mml-secure-openpgp-always-trust t
+ "If t, skip key validation of GnuPG on encryption."
+ :group 'mime-security
+ :type 'boolean)
+
+(defcustom mml-secure-fail-when-key-problem nil
+ "If t, raise an error if some key is missing or several keys exist.
+Otherwise, ask the user."
+ :group 'mime-security
+ :type 'boolean)
+
+(defcustom mml-secure-key-preferences
+ '((OpenPGP (sign) (encrypt)) (CMS (sign) (encrypt)))
+ "Protocol- and usage-specific fingerprints of preferred keys.
+This variable is only relevant if a recipient owns multiple key pairs (for
+encryption) or you own multiple key pairs (for signing). In such cases,
+you will be asked which key(s) should be used, and your choice can be
+customized in this variable."
+ :group 'mime-security
+ :type '(alist :key-type (symbol :tag "Protocol") :value-type
+ (alist :key-type (symbol :tag "Usage") :value-type
+ (alist :key-type (string :tag "Name") :value-type
+ (repeat (string :tag "Fingerprint"))))))
+
+(defun mml-secure-cust-usage-lookup (context usage)
+ "Return preferences for CONTEXT and USAGE."
+ (let* ((protocol (epg-context-protocol context))
+ (protocol-prefs (cdr (assoc protocol mml-secure-key-preferences))))
+ (assoc usage protocol-prefs)))
+
+(defun mml-secure-cust-fpr-lookup (context usage name)
+ "Return fingerprints of preferred keys for CONTEXT, USAGE, and NAME."
+ (let* ((usage-prefs (mml-secure-cust-usage-lookup context usage))
+ (fprs (assoc name (cdr usage-prefs))))
+ (when fprs
+ (cdr fprs))))
+
+(defun mml-secure-cust-record-keys (context usage name keys &optional save)
+ "For CONTEXT, USAGE, and NAME record fingerprint(s) of KEYS.
+If optional SAVE is not nil, save customized fingerprints.
+Return keys."
+ (assert keys)
+ (let* ((usage-prefs (mml-secure-cust-usage-lookup context usage))
+ (curr-fprs (cdr (assoc name (cdr usage-prefs))))
+ (key-fprs (mapcar 'mml-secure-fingerprint keys))
+ (new-fprs (gnus-union curr-fprs key-fprs :test 'equal)))
+ (if curr-fprs
+ (setcdr (assoc name (cdr usage-prefs)) new-fprs)
+ (setcdr usage-prefs (cons (cons name new-fprs) (cdr usage-prefs))))
+ (when save
+ (customize-save-variable
+ 'mml-secure-key-preferences mml-secure-key-preferences))
+ keys))
+
+(defun mml-secure-cust-remove-keys (context usage name)
+ "Remove keys for CONTEXT, USAGE, and NAME.
+Return t if a customization for NAME was present (and has been removed)."
+ (let* ((usage-prefs (mml-secure-cust-usage-lookup context usage))
+ (current (assoc name usage-prefs)))
+ (when current
+ (setcdr usage-prefs (remove current (cdr usage-prefs)))
+ t)))
+
+(defvar mml-secure-secret-key-id-list nil)
+
+(defun mml-secure-add-secret-key-id (key-id)
+ "Record KEY-ID in list of secret keys."
+ (add-to-list 'mml-secure-secret-key-id-list key-id))
+
+(defun mml-secure-clear-secret-key-id-list ()
+ "Remove passwords from cache and clear list of secret keys."
+ ;; Loosely based on code inside mml2015-epg-encrypt,
+ ;; mml2015-epg-clear-decrypt, and mml2015-epg-decrypt
+ (dolist (key-id mml-secure-secret-key-id-list nil)
+ (password-cache-remove key-id))
+ (setq mml-secure-secret-key-id-list nil))
+
+(defvar mml1991-cache-passphrase)
+(defvar mml1991-passphrase-cache-expiry)
+
+(defun mml-secure-cache-passphrase-p (protocol)
+ "Return t if OpenPGP or S/MIME passphrases should be cached for PROTOCOL.
+Passphrase caching in Emacs is NOT recommended. Use gpg-agent instead."
+ (or (and (eq 'OpenPGP protocol)
+ (or mml-secure-cache-passphrase
+ (and (boundp 'mml2015-cache-passphrase)
+ mml2015-cache-passphrase)
+ (and (boundp 'mml1991-cache-passphrase)
+ mml1991-cache-passphrase)))
+ (and (eq 'CMS protocol)
+ (or mml-secure-cache-passphrase
+ (and (boundp 'mml-smime-cache-passphrase)
+ mml-smime-cache-passphrase)))))
+
+(defun mml-secure-cache-expiry-interval (protocol)
+ "Return time in seconds to cache passphrases for PROTOCOL.
+Passphrase caching in Emacs is NOT recommended. Use gpg-agent instead."
+ (or (and (eq 'OpenPGP protocol)
+ (or (and (boundp 'mml2015-passphrase-cache-expiry)
+ mml2015-passphrase-cache-expiry)
+ (and (boundp 'mml1991-passphrase-cache-expiry)
+ mml1991-passphrase-cache-expiry)
+ mml-secure-passphrase-cache-expiry))
+ (and (eq 'CMS protocol)
+ (or (and (boundp 'mml-smime-passphrase-cache-expiry)
+ mml-smime-passphrase-cache-expiry)
+ mml-secure-passphrase-cache-expiry))))
+
+(defun mml-secure-passphrase-callback (context key-id standard)
+ "Ask for passphrase in CONTEXT for KEY-ID for STANDARD.
+The passphrase is read and cached."
+ ;; Based on mml2015-epg-passphrase-callback.
+ (if (eq key-id 'SYM)
+ (epg-passphrase-callback-function context key-id nil)
+ (let* ((password-cache-key-id
+ (if (eq key-id 'PIN)
+ "PIN"
+ key-id))
+ (entry (assoc key-id epg-user-id-alist))
+ (passphrase
+ (password-read
+ (if (eq key-id 'PIN)
+ "Passphrase for PIN: "
+ (if entry
+ (format "Passphrase for %s %s: " key-id (cdr entry))
+ (format "Passphrase for %s: " key-id)))
+ ;; TODO: With mml-smime.el, password-cache-key-id is not passed
+ ;; as argument to password-read.
+ ;; Is that on purpose? If so, the following needs to be placed
+ ;; inside an if statement.
+ password-cache-key-id)))
+ (when passphrase
+ (let ((password-cache-expiry (mml-secure-cache-expiry-interval
+ (epg-context-protocol context))))
+ (password-cache-add password-cache-key-id passphrase))
+ (mml-secure-add-secret-key-id password-cache-key-id)
+ (copy-sequence passphrase)))))
+
+(defun mml-secure-check-user-id (key recipient)
+ "Check whether KEY has a non-revoked, non-expired UID for RECIPIENT."
+ ;; Based on mml2015-epg-check-user-id.
+ (let ((uids (epg-key-user-id-list key)))
+ (catch 'break
+ (dolist (uid uids nil)
+ (if (and (stringp (epg-user-id-string uid))
+ (equal (car (mail-header-parse-address
+ (epg-user-id-string uid)))
+ (car (mail-header-parse-address
+ recipient)))
+ (not (memq (epg-user-id-validity uid)
+ '(revoked expired))))
+ (throw 'break t))))))
+
+(defun mml-secure-secret-key-exists-p (context subkey)
+ "Return t if keyring for CONTEXT contains secret key for public SUBKEY."
+ (let* ((fpr (epg-sub-key-fingerprint subkey))
+ (candidates (epg-list-keys context fpr 'secret))
+ (candno (length candidates)))
+ ;; If two or more subkeys with the same fingerprint exist, something is
+ ;; terribly wrong.
+ (when (>= candno 2)
+ (error "Found %d secret keys with same fingerprint %s" candno fpr))
+ (= 1 candno)))
+
+(defun mml-secure-check-sub-key (context key usage &optional fingerprint)
+ "Check whether in CONTEXT the public KEY has a usable subkey for USAGE.
+This is the case if KEY is not disabled, and there is a subkey for
+USAGE that is neither revoked nor expired. Additionally, if optional
+FINGERPRINT is present and if it is not the primary key's fingerprint, then
+the returned subkey must have that FINGERPRINT. FINGERPRINT must consist of
+hexadecimal digits only (no leading \"0x\" allowed).
+If USAGE is not `encrypt', then additionally an appropriate secret key must
+be present in the keyring."
+ ;; Based on mml2015-epg-check-sub-key, extended by
+ ;; - check for secret keys if usage is not 'encrypt and
+ ;; - check for new argument FINGERPRINT.
+ (let* ((subkeys (epg-key-sub-key-list key))
+ (primary (car subkeys))
+ (fpr (epg-sub-key-fingerprint primary)))
+ ;; The primary key will be marked as disabled, when the entire
+ ;; key is disabled (see 12 Field, Format of colon listings, in
+ ;; gnupg/doc/DETAILS)
+ (unless (memq 'disabled (epg-sub-key-capability primary))
+ (catch 'break
+ (dolist (subkey subkeys nil)
+ (if (and (memq usage (epg-sub-key-capability subkey))
+ (not (memq (epg-sub-key-validity subkey)
+ '(revoked expired)))
+ (or (eq 'encrypt usage) ; Encryption works with public key.
+ ;; In contrast, signing requires secret key.
+ (mml-secure-secret-key-exists-p context subkey))
+ (or (not fingerprint)
+ (gnus-string-match-p (concat fingerprint "$") fpr)
+ (gnus-string-match-p (concat fingerprint "$")
+ (epg-sub-key-fingerprint subkey))))
+ (throw 'break t)))))))
+
+(defun mml-secure-find-usable-keys (context name usage &optional justone)
+ "In CONTEXT return a list of keys for NAME and USAGE.
+If USAGE is `encrypt' public keys are returned, otherwise secret ones.
+Only non-revoked and non-expired keys are returned whose primary key is
+not disabled.
+NAME can be an e-mail address or a key ID.
+If NAME just consists of hexadecimal digits (possibly prefixed by \"0x\"), it
+is treated as key ID for which at most one key must exist in the keyring.
+Otherwise, NAME is treated as user ID, for which no keys are returned if it
+is expired or revoked.
+If optional JUSTONE is not nil, return the first key instead of a list."
+ (let* ((keys (epg-list-keys context name))
+ (iskeyid (string-match "\\(0x\\)?\\([0-9a-fA-F]\\{8,\\}\\)" name))
+ (fingerprint (match-string 2 name))
+ result)
+ (when (and iskeyid (>= (length keys) 2))
+ (error
+ "Name %s (for %s) looks like a key ID but multiple keys found"
+ name usage))
+ (catch 'break
+ (dolist (key keys result)
+ (if (and (or iskeyid
+ (mml-secure-check-user-id key name))
+ (mml-secure-check-sub-key context key usage fingerprint))
+ (if justone
+ (throw 'break key)
+ (push key result)))))))
+
+(defun mml-secure-select-preferred-keys (context names usage)
+ "Return list of preferred keys in CONTEXT for NAMES and USAGE.
+This inspects the keyrings to find keys for each name in NAMES. If several
+keys are found for a name, `mml-secure-select-keys' is used to look for
+customized preferences or have the user select preferable ones.
+When `mml-secure-fail-when-key-problem' is t, fail with an error in
+case of missing, outdated, or multiple keys."
+ ;; Loosely based on code appearing inside mml2015-epg-sign and
+ ;; mml2015-epg-encrypt.
+ (apply
+ #'nconc
+ (mapcar
+ (lambda (name)
+ (let* ((keys (mml-secure-find-usable-keys context name usage))
+ (keyno (length keys)))
+ (cond ((= 0 keyno)
+ (when (or mml-secure-fail-when-key-problem
+ (not (y-or-n-p
+ (format "No %s key for %s; skip it? "
+ usage name))))
+ (error "No %s key for %s" usage name)))
+ ((= 1 keyno) keys)
+ (t (mml-secure-select-keys context name keys usage)))))
+ names)))
+
+(defun mml-secure-fingerprint (key)
+ "Return fingerprint for public KEY."
+ (epg-sub-key-fingerprint (car (epg-key-sub-key-list key))))
+
+(defun mml-secure-filter-keys (keys fprs)
+ "Filter KEYS to subset with fingerprints in FPRS."
+ (when keys
+ (if (member (mml-secure-fingerprint (car keys)) fprs)
+ (cons (car keys) (mml-secure-filter-keys (cdr keys) fprs))
+ (mml-secure-filter-keys (cdr keys) fprs))))
+
+(defun mml-secure-normalize-cust-name (name)
+ "Normalize NAME to be used for customization.
+Currently, remove ankle brackets."
+ (if (string-match "^<\\(.*\\)>$" name)
+ (match-string 1 name)
+ name))
+
+(defun mml-secure-select-keys (context name keys usage)
+ "In CONTEXT for NAME select among KEYS for USAGE.
+KEYS should be a list with multiple entries.
+NAME is normalized first as customized keys are inspected.
+When `mml-secure-fail-when-key-problem' is t, fail with an error in case of
+outdated or multiple keys."
+ (let* ((nname (mml-secure-normalize-cust-name name))
+ (fprs (mml-secure-cust-fpr-lookup context usage nname))
+ (usable-fprs (mapcar 'mml-secure-fingerprint keys)))
+ (if fprs
+ (if (gnus-subsetp fprs usable-fprs)
+ (mml-secure-filter-keys keys fprs)
+ (mml-secure-cust-remove-keys context usage nname)
+ (let ((diff (gnus-setdiff fprs usable-fprs)))
+ (if mml-secure-fail-when-key-problem
+ (error "Customization of %s keys for %s outdated" usage nname)
+ (mml-secure-select-keys-1
+ context nname keys usage (format "\
+Customized keys
+ (%s)
+for %s not available any more.
+Select anew. "
+ diff nname)))))
+ (if mml-secure-fail-when-key-problem
+ (error "Multiple %s keys for %s" usage nname)
+ (mml-secure-select-keys-1
+ context nname keys usage (format "\
+Multiple %s keys for:
+ %s
+Select preferred one(s). "
+ usage nname))))))
+
+(defun mml-secure-select-keys-1 (context name keys usage message)
+ "In CONTEXT for NAME let user select among KEYS for USAGE, showing MESSAGE.
+Return selected keys."
+ (let* ((selected (epa--select-keys message keys))
+ (selno (length selected))
+ ;; TODO: y-or-n-p does not always resize the echo area but may
+ ;; truncate the message. Why? The following does not help.
+ ;; yes-or-no-p shows full message, though.
+ (message-truncate-lines nil))
+ (if selected
+ (if (y-or-n-p
+ (format "%d %s key(s) selected. Store for %s? "
+ selno usage name))
+ (mml-secure-cust-record-keys context usage name selected 'save)
+ selected)
+ (unless (y-or-n-p
+ (format "No %s key for %s; skip it? " usage name))
+ (error "No %s key for %s" usage name)))))
+
+(defun mml-secure-signer-names (protocol sender)
+ "Determine signer names for PROTOCOL and message from SENDER.
+Returned names may be e-mail addresses or key IDs and are determined based
+on `mml-secure-openpgp-signers' and `mml-secure-openpgp-sign-with-sender' with
+OpenPGP or `mml-secure-smime-signers' and `mml-secure-smime-sign-with-sender'
+with S/MIME."
+ (if (eq 'OpenPGP protocol)
+ (append mml-secure-openpgp-signers
+ (if (and mml-secure-openpgp-sign-with-sender sender)
+ (list (concat "<" sender ">"))))
+ (append mml-secure-smime-signers
+ (if (and mml-secure-smime-sign-with-sender sender)
+ (list (concat "<" sender ">"))))))
+
+(defun mml-secure-signers (context signer-names)
+ "Determine signing keys in CONTEXT from SIGNER-NAMES.
+If `mm-sign-option' is `guided', the user is asked to choose.
+Otherwise, `mml-secure-select-preferred-keys' is used."
+ ;; Based on code appearing inside mml2015-epg-sign and
+ ;; mml2015-epg-encrypt.
+ (if (eq mm-sign-option 'guided)
+ (epa-select-keys context "\
+Select keys for signing.
+If no one is selected, default secret key is used. "
+ signer-names t)
+ (mml-secure-select-preferred-keys context signer-names 'sign)))
+
+(defun mml-secure-self-recipients (protocol sender)
+ "Determine additional recipients based on encrypt-to-self variables.
+PROTOCOL specifies OpenPGP or S/MIME for a message from SENDER."
+ (let ((encrypt-to-self
+ (if (eq 'OpenPGP protocol)
+ mml-secure-openpgp-encrypt-to-self
+ mml-secure-smime-encrypt-to-self)))
+ (when encrypt-to-self
+ (if (listp encrypt-to-self)
+ encrypt-to-self
+ (list sender)))))
+
+(defun mml-secure-recipients (protocol context config sender)
+ "Determine encryption recipients.
+PROTOCOL specifies OpenPGP or S/MIME with matching CONTEXT and CONFIG
+for a message from SENDER."
+ ;; Based on code appearing inside mml2015-epg-encrypt.
+ (let ((recipients
+ (apply #'nconc
+ (mapcar
+ (lambda (recipient)
+ (or (epg-expand-group config recipient)
+ (list (concat "<" recipient ">"))))
+ (split-string
+ (or (message-options-get 'message-recipients)
+ (message-options-set 'message-recipients
+ (read-string "Recipients: ")))
+ "[ \f\t\n\r\v,]+")))))
+ (nconc recipients (mml-secure-self-recipients protocol sender))
+ (if (eq mm-encrypt-option 'guided)
+ (setq recipients
+ (epa-select-keys context "\
+Select recipients for encryption.
+If no one is selected, symmetric encryption will be performed. "
+ recipients))
+ (setq recipients
+ (mml-secure-select-preferred-keys context recipients 'encrypt))
+ (unless recipients
+ (error "No recipient specified")))
+ recipients))
+
+(defun mml-secure-epg-encrypt (protocol cont &optional sign)
+ ;; Based on code appearing inside mml2015-epg-encrypt.
+ (let* ((context (epg-make-context protocol))
+ (config (epg-configuration))
+ (sender (message-options-get 'message-sender))
+ (recipients (mml-secure-recipients protocol context config sender))
+ (signer-names (mml-secure-signer-names protocol sender))
+ cipher signers)
+ (when sign
+ (setq signers (mml-secure-signers context signer-names))
+ (epg-context-set-signers context signers))
+ (when (eq 'OpenPGP protocol)
+ (epg-context-set-armor context t)
+ (epg-context-set-textmode context t))
+ (when (mml-secure-cache-passphrase-p protocol)
+ (epg-context-set-passphrase-callback
+ context
+ (cons 'mml-secure-passphrase-callback protocol)))
+ (condition-case error
+ (setq cipher
+ (if (eq 'OpenPGP protocol)
+ (epg-encrypt-string context (buffer-string) recipients sign
+ mml-secure-openpgp-always-trust)
+ (epg-encrypt-string context (buffer-string) recipients))
+ mml-secure-secret-key-id-list nil)
+ (error
+ (mml-secure-clear-secret-key-id-list)
+ (signal (car error) (cdr error))))
+ cipher))
+
+(defun mml-secure-epg-sign (protocol mode)
+ ;; Based on code appearing inside mml2015-epg-sign.
+ (let* ((context (epg-make-context protocol))
+ (sender (message-options-get 'message-sender))
+ (signer-names (mml-secure-signer-names protocol sender))
+ (signers (mml-secure-signers context signer-names))
+ signature micalg)
+ (when (eq 'OpenPGP protocol)
+ (epg-context-set-armor context t)
+ (epg-context-set-textmode context t))
+ (epg-context-set-signers context signers)
+ (when (mml-secure-cache-passphrase-p protocol)
+ (epg-context-set-passphrase-callback
+ context
+ (cons 'mml-secure-passphrase-callback protocol)))
+ (condition-case error
+ (setq signature
+ (if (eq 'OpenPGP protocol)
+ (epg-sign-string context (buffer-string) mode)
+ (epg-sign-string context
+ (mm-replace-in-string (buffer-string)
+ "\n" "\r\n") t))
+ mml-secure-secret-key-id-list nil)
+ (error
+ (mml-secure-clear-secret-key-id-list)
+ (signal (car error) (cdr error))))
+ (if (epg-context-result-for context 'sign)
+ (setq micalg (epg-new-signature-digest-algorithm
+ (car (epg-context-result-for context 'sign)))))
+ (cons signature micalg)))
+
(provide 'mml-sec)
;;; mml-sec.el ends here