Mercurial > hg > xemacs-beta
diff lisp/mailcrypt/mc-pgp.el @ 0:376386a54a3c r19-14
Import from CVS: tag r19-14
author | cvs |
---|---|
date | Mon, 13 Aug 2007 08:45:50 +0200 |
parents | |
children | ec9a17fef872 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/mailcrypt/mc-pgp.el Mon Aug 13 08:45:50 2007 +0200 @@ -0,0 +1,620 @@ +;; mc-pgp.el, PGP support for Mailcrypt +;; Copyright (C) 1995 Jin Choi <jin@atype.com> +;; Patrick LoPresti <patl@lcs.mit.edu> + +;;{{{ Licensing +;; This file is intended to be used with GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +;;}}} +(require 'mailcrypt) + +(defvar mc-pgp-user-id (user-login-name) + "*PGP ID of your default identity.") +(defvar mc-pgp-always-sign nil + "*If t, always sign encrypted PGP messages, or never sign if 'never.") +(defvar mc-pgp-path "pgp" "*The PGP executable.") +(defvar mc-pgp-display-snarf-output nil + "*If t, pop up the PGP output window when snarfing keys.") +(defvar mc-pgp-alternate-keyring nil + "*Public keyring to use instead of default.") +(defvar mc-pgp-comment + (format "Processed by Mailcrypt %s, an Emacs/PGP interface" mc-version) + "*Comment field to appear in ASCII armor output. If nil, let PGP +use its default.") + +(defconst mc-pgp-msg-begin-line "-----BEGIN PGP MESSAGE-----" + "Text for start of PGP message delimiter.") +(defconst mc-pgp-msg-end-line "-----END PGP MESSAGE-----\n?" + "Text for end of PGP message delimiter.") +(defconst mc-pgp-signed-begin-line "-----BEGIN PGP SIGNED MESSAGE-----" + "Text for start of PGP signed messages.") +(defconst mc-pgp-signed-end-line "-----END PGP SIGNATURE-----" + "Text for end of PGP signed messages.") +(defconst mc-pgp-key-begin-line "^-----BEGIN PGP PUBLIC KEY BLOCK-----\r?$" + "Text for start of PGP public key.") +(defconst mc-pgp-key-end-line "^-----END PGP PUBLIC KEY BLOCK-----\r?$" + "Text for end of PGP public key.") +(defconst mc-pgp-error-re "^\\(ERROR:\\|WARNING:\\).*" + "Regular expression matching an error from PGP") +(defconst mc-pgp-sigok-re "^.*Good signature.*" + "Regular expression matching a PGP signature validation message") +(defconst mc-pgp-newkey-re "^[ \t]*\\(No\\|[0-9]+\\) +new [ku].*" + "Regular expression matching a PGP key snarf message") +(defconst mc-pgp-nokey-re + "Cannot find the public key matching userid '\\(.+\\)'$" + "Regular expression matching a PGP missing-key messsage") +(defconst mc-pgp-key-expected-re + "Key matching expected Key ID \\(\\S +\\) not found") + +(defvar mc-pgp-keydir nil + "Directory in which keyrings are stored.") + +(defun mc-get-pgp-keydir () + (if (null mc-pgp-keydir) + (let ((buffer (generate-new-buffer " *mailcrypt temp*")) + (obuf (current-buffer))) + (unwind-protect + (progn + (call-process mc-pgp-path nil buffer nil "+verbose=1" + "+language=en" "-kv" "XXXXXXXXXX") + (set-buffer buffer) + (goto-char (point-min)) + (re-search-forward "^Key ring:\\s *'\\(.*\\)'") + (setq mc-pgp-keydir + (file-name-directory + (buffer-substring-no-properties + (match-beginning 1) (match-end 1))))) + (set-buffer obuf) + (kill-buffer buffer)))) + mc-pgp-keydir) + +(defvar mc-pgp-key-cache nil + "Association list mapping PGP IDs to canonical \"keys\". A \"key\" +is a pair (USER-ID . KEY-ID) which identifies the canonical IDs of the +PGP ID.") + +(defun mc-pgp-lookup-key (str) + ;; Look up the string STR in the user's secret key ring. Return a + ;; pair of strings (USER-ID . KEY-ID) which uniquely identifies the + ;; matching key, or nil if no key matches. + (if (equal str "***** CONVENTIONAL *****") nil + (let ((keyring (concat (mc-get-pgp-keydir) "secring")) + (result (cdr-safe (assoc str mc-pgp-key-cache))) + (key-regexp + "^\\(pub\\|sec\\)\\s +[^/]+/\\(\\S *\\)\\s +\\S +\\s +\\(.*\\)$") + (obuf (current-buffer)) + buffer) + (if (null result) + (unwind-protect + (progn + (setq buffer (generate-new-buffer " *mailcrypt temp")) + (call-process mc-pgp-path nil buffer nil + "+language=en" "-kv" str keyring) + (set-buffer buffer) + (goto-char (point-min)) + (if (re-search-forward key-regexp nil t) + (progn + (setq result + (cons (buffer-substring-no-properties + (match-beginning 3) (match-end 3)) + (concat + "0x" + (buffer-substring-no-properties + (match-beginning 2) (match-end 2))))) + (setq mc-pgp-key-cache (cons (cons str result) + mc-pgp-key-cache))))) + (if buffer (kill-buffer buffer)) + (set-buffer obuf))) + (if (null result) + (error "No PGP secret key for %s" str)) + result))) + +(defun mc-pgp-generic-parser (result) + (let (start) + (goto-char (point-min)) + (cond ((not (eq result 0)) + (prog1 + nil + (if (mc-message "^\aError: +Bad pass phrase\\.$" (current-buffer)) + (mc-deactivate-passwd t) + (mc-message mc-pgp-error-re (current-buffer) + (format "PGP exited with status %d" result))))) + ((re-search-forward mc-pgp-nokey-re nil t) + nil) + (t + (and + (goto-char (point-min)) + (re-search-forward "-----BEGIN PGP.*-----$" nil t) + (setq start (match-beginning 0)) + (goto-char (point-max)) + (re-search-backward "^-----END PGP.*-----\n" nil t) + (cons start (match-end 0))))))) + +(defun mc-pgp-encrypt-region (recipients start end &optional id sign) + (let ((process-environment process-environment) + (buffer (get-buffer-create mc-buffer-name)) + ;; Crock. Rewrite someday. + (mc-pgp-always-sign mc-pgp-always-sign) + (obuf (current-buffer)) + action msg args key passwd result pgp-id) + (setq args (list "+encrypttoself=off +verbose=1" "+batchmode" + "+language=en" "-fat")) + (setq action (if recipients "Encrypting" "Armoring")) + (setq msg (format "%s..." action)) ; May get overridden below + (if recipients (setq args (cons "-e" args))) + (if mc-pgp-comment + (setq args (cons (format "+comment=%s" mc-pgp-comment) args))) + (if mc-pgp-alternate-keyring + (setq args (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + (if (and (not (eq mc-pgp-always-sign 'never)) + (or mc-pgp-always-sign sign (y-or-n-p "Sign the message? "))) + (progn + (setq mc-pgp-always-sign t) + (setq key (mc-pgp-lookup-key (or id mc-pgp-user-id))) + (setq passwd + (mc-activate-passwd + (cdr key) + (format "PGP passphrase for %s (%s): " (car key) (cdr key)))) + (setq args + (nconc args (list "-s" "-u" (cdr key)))) + (setenv "PGPPASSFD" "0") + (setq msg (format "%s+signing as %s ..." action (car key)))) + (setq mc-pgp-always-sign 'never)) + + (or key + (setq key (mc-pgp-lookup-key mc-pgp-user-id))) + + (if (and recipients mc-encrypt-for-me) + (setq recipients (cons (cdr key) recipients))) + + (setq args (append args recipients)) + + (message "%s" msg) + (setq result (mc-process-region start end passwd mc-pgp-path args + 'mc-pgp-generic-parser buffer)) + (save-excursion + (set-buffer buffer) + (goto-char (point-min)) + (if (re-search-forward mc-pgp-nokey-re nil t) + (progn + (if result (error "This should never happen.")) + (setq pgp-id (buffer-substring-no-properties + (match-beginning 1) (match-end 1))) + (if (and (not (eq mc-pgp-always-fetch 'never)) + (or mc-pgp-always-fetch + (y-or-n-p + (format "Key for '%s' not found; try to fetch? " + pgp-id)))) + (progn + (mc-pgp-fetch-key (cons pgp-id nil)) + (set-buffer obuf) + (mc-pgp-encrypt-region recipients start end id)) + (mc-message mc-pgp-nokey-re buffer) + nil)) + (if (not result) + nil + (message "%s Done." msg) + t))))) + +(defun mc-pgp-decrypt-parser (result) + (goto-char (point-min)) + (cond ((eq result 0) + ;; Valid signature + (re-search-forward "^Signature made.*\n") + (if (looking-at + "\a\nWARNING: Because this public key.*\n.*\n.*\n") + (goto-char (match-end 0))) + (cons (point) (point-max))) + ((eq result 1) + (re-search-forward + "\\(\\(^File is conven.*\\)?Just a moment\\.+\\)\\|\\(^\\.\\)") + (if (eq (match-beginning 2) (match-end 2)) + (if (looking-at + "\nFile has signature.*\\(\n\a.*\n\\)*\nWARNING:.*\n") + (goto-char (match-end 0))) + (if (looking-at "Pass phrase appears good\\. \\.") + (goto-char (match-end 0)))) + (cons (point) (point-max))) + (t nil))) + +(defun mc-pgp-decrypt-region (start end &optional id) + ;; returns a pair (SUCCEEDED . VERIFIED) where SUCCEEDED is t if + ;; the decryption succeeded and verified is t if there was a valid signature + (let ((process-environment process-environment) + (buffer (get-buffer-create mc-buffer-name)) + args key new-key passwd result pgp-id) + (undo-boundary) + (setq key (mc-pgp-lookup-key (or id mc-pgp-user-id))) + (setq + passwd + (if key + (mc-activate-passwd (cdr key) + (and id + (format "PGP passphrase for %s (%s): " + (car key) (cdr key)))) + (mc-activate-passwd id "PGP passphrase for conventional decryption: "))) + (if passwd + (setenv "PGPPASSFD" "0")) + (setq args '("+verbose=1" "+batchmode" "+language=en" "-f")) + (if mc-pgp-alternate-keyring + (setq args (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + (message "Decrypting...") + (setq result + (mc-process-region + start end passwd mc-pgp-path args 'mc-pgp-decrypt-parser buffer)) + (cond + (result + (message "Decrypting... Done.") + ;; If verification failed due to missing key, offer to fetch it. + (save-excursion + (set-buffer buffer) + (goto-char (point-min)) + (if (re-search-forward mc-pgp-key-expected-re nil t) + (setq pgp-id (concat "0x" (buffer-substring-no-properties + (match-beginning 1) + (match-end 1)))))) + (if (and pgp-id + (not (eq mc-pgp-always-fetch 'never)) + (or mc-pgp-always-fetch + (y-or-n-p + (format "Key %s not found; attempt to fetch? " pgp-id))) + (mc-pgp-fetch-key (cons nil pgp-id))) + (progn + (undo-start) + (undo-more 1) + (mc-pgp-decrypt-region start end id)) + (mc-message mc-pgp-key-expected-re buffer) + (cons t (eq result 0)))) + ;; Decryption failed; maybe we need to use a different user-id + ((save-excursion + (and + (set-buffer buffer) + (goto-char (point-min)) + (re-search-forward + "^Key for user ID:.*\n.*Key ID \\([0-9A-F]+\\)" nil t) + (setq new-key + (mc-pgp-lookup-key + (concat "0x" (buffer-substring-no-properties + (match-beginning 1) + (match-end 1))))) + (not (and id (equal key new-key))))) + (mc-pgp-decrypt-region start end (cdr new-key))) + ;; Or maybe it is conventionally encrypted + ((save-excursion + (and + (set-buffer buffer) + (goto-char (point-min)) + (re-search-forward "^File is conventionally encrypted" nil t))) + (if (null key) (mc-deactivate-passwd t)) + (mc-pgp-decrypt-region start end "***** CONVENTIONAL *****")) + (t + (mc-display-buffer buffer) + (if (mc-message "^\aError: +Bad pass phrase\\.$" buffer) + (mc-deactivate-passwd t) + (mc-message mc-pgp-error-re buffer "Error decrypting buffer")) + (cons nil nil))))) + +(defun mc-pgp-sign-region (start end &optional id unclear) + (let ((process-environment process-environment) + (buffer (get-buffer-create mc-buffer-name)) + passwd args key) + (setq key (mc-pgp-lookup-key (or id mc-pgp-user-id))) + (setq passwd + (mc-activate-passwd + (cdr key) + (format "PGP passphrase for %s (%s): " (car key) (cdr key)))) + (setenv "PGPPASSFD" "0") + (setq args + (list + "-fast" "+verbose=1" "+language=en" + (format "+clearsig=%s" (if unclear "off" "on")) + "+batchmode" "-u" (cdr key))) + (if mc-pgp-comment + (setq args (cons (format "+comment=%s" mc-pgp-comment) args))) + (message "Signing as %s ..." (car key)) + (if (mc-process-region start end passwd mc-pgp-path args + 'mc-pgp-generic-parser buffer) + (progn + (message "Signing as %s ... Done." (car key)) + t) + nil))) + +(defun mc-pgp-verify-parser (result) + (cond ((eq result 0) + (mc-message mc-pgp-sigok-re (current-buffer) "Good signature") + t) + ((eq result 1) + (mc-message mc-pgp-error-re (current-buffer) "Bad signature") + nil) + (t + (mc-message mc-pgp-error-re (current-buffer) + (format "PGP exited with status %d" result)) + nil))) + +(defun mc-pgp-verify-region (start end &optional no-fetch) + (let ((buffer (get-buffer-create mc-buffer-name)) + (obuf (current-buffer)) + args pgp-id) + (setq args '("+verbose=1" "+batchmode" "+language=en" "-f")) + (if mc-pgp-alternate-keyring + (setq args (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + (message "Verifying...") + (if (mc-process-region + start end nil mc-pgp-path args 'mc-pgp-verify-parser buffer) + t + (save-excursion + (set-buffer buffer) + (goto-char (point-min)) + (if (and + (not no-fetch) + (re-search-forward mc-pgp-key-expected-re nil t) + (setq pgp-id + (concat "0x" (buffer-substring-no-properties + (match-beginning 1) + (match-end 1)))) + (not (eq mc-pgp-always-fetch 'never)) + (or mc-pgp-always-fetch + (y-or-n-p + (format "Key %s not found; attempt to fetch? " pgp-id))) + (mc-pgp-fetch-key (cons nil pgp-id)) + (set-buffer obuf)) + (mc-pgp-verify-region start end t) + (mc-message mc-pgp-error-re buffer) + nil))))) + +(defun mc-pgp-insert-public-key (&optional id) + (let ((buffer (get-buffer-create mc-buffer-name)) + args) + (setq id (or id mc-pgp-user-id)) + (setq args (list "+verbose=1" "+batchmode" "+language=en" "-kxaf" id)) + (if mc-pgp-comment + (setq args (cons (format "+comment=%s" mc-pgp-comment) args))) + (if mc-pgp-alternate-keyring + (setq args (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + + (if (mc-process-region (point) (point) nil mc-pgp-path + args 'mc-pgp-generic-parser buffer) + (progn + (mc-message "Key for user ID: .*" buffer) + t)))) + +(defun mc-pgp-snarf-parser (result) + (eq result 0)) + +(defun mc-pgp-snarf-keys (start end) + ;; Returns number of keys found. + (let ((buffer (get-buffer-create mc-buffer-name)) tmpstr args) + (setq args '("+verbose=1" "+batchmode" "+language=en" "-kaf")) + (if mc-pgp-alternate-keyring + (setq args (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + (message "Snarfing...") + (if (mc-process-region start end nil mc-pgp-path args + 'mc-pgp-snarf-parser buffer) + (save-excursion + (set-buffer buffer) + (goto-char (point-min)) + (if (re-search-forward mc-pgp-newkey-re nil t) + (progn + (if mc-pgp-display-snarf-output (mc-display-buffer buffer)) + (setq tmpstr (buffer-substring-no-properties + (match-beginning 1) + (match-end 1))) + (if (equal tmpstr "No") + 0 + (car (read-from-string tmpstr)))))) + (mc-display-buffer buffer) + (mc-message mc-pgp-error-re buffer "Error snarfing PGP keys") + 0))) + +;;;###autoload +(defun mc-scheme-pgp () + (list + (cons 'encryption-func 'mc-pgp-encrypt-region) + (cons 'decryption-func 'mc-pgp-decrypt-region) + (cons 'signing-func 'mc-pgp-sign-region) + (cons 'verification-func 'mc-pgp-verify-region) + (cons 'key-insertion-func 'mc-pgp-insert-public-key) + (cons 'snarf-func 'mc-pgp-snarf-keys) + (cons 'msg-begin-line mc-pgp-msg-begin-line) + (cons 'msg-end-line mc-pgp-msg-end-line) + (cons 'signed-begin-line mc-pgp-signed-begin-line) + (cons 'signed-end-line mc-pgp-signed-end-line) + (cons 'key-begin-line mc-pgp-key-begin-line) + (cons 'key-end-line mc-pgp-key-end-line) + (cons 'user-id mc-pgp-user-id))) + +;;{{{ Key fetching + +(defvar mc-pgp-always-fetch nil + "*If t, always attempt to fetch missing keys, or never fetch if +'never.") + +(defvar mc-pgp-keyserver-url-template + "/htbin/pks-extract-key.pl?op=get&search=%s" + "The URL to pass to the keyserver.") + +(defvar mc-pgp-keyserver-address "pgp.ai.mit.edu" + "Host name of keyserver.") + +(defvar mc-pgp-keyserver-port 80 + "Port on which the keyserver's HTTP daemon lives.") + +(defvar mc-pgp-fetch-timeout 20 + "*Timeout, in seconds, for any particular key fetch operation.") + +(defvar mc-pgp-fetch-keyring-list nil + "*List of strings which are filenames of public keyrings to search +when fetching keys.") + +(defsubst mc-pgp-buffer-get-key (buf) + "Return the first key block in BUF as a string, or nil if none found." + (save-excursion + (let (start) + (set-buffer buf) + (goto-char (point-min)) + (and (re-search-forward mc-pgp-key-begin-line nil t) + (setq start (match-beginning 0)) + (re-search-forward mc-pgp-key-end-line nil t) + (buffer-substring-no-properties start (match-end 0)))))) + +(defun mc-pgp-fetch-from-keyrings (id) + (let ((keyring-list mc-pgp-fetch-keyring-list) + buf proc key) + (unwind-protect + (progn + (message "Fetching %s from keyrings..." (or (cdr id) (car id))) + (while (and (not key) keyring-list) + (setq buf (generate-new-buffer " *mailcrypt temp*")) + (setq proc + (start-process "*PGP*" buf mc-pgp-path "-kxaf" + "+verbose=0" "+batchmode" + (format "+pubring=%s" (car keyring-list)) + (or (cdr id) (car id)))) + ;; Because PGPPASSFD might be set + (process-send-string proc "\r\n") + (while (eq 'run (process-status proc)) + (accept-process-output proc 5)) + (setq key (mc-pgp-buffer-get-key buf)) + (setq keyring-list (cdr keyring-list))) + key) + (if buf (kill-buffer buf)) + (if (and proc (eq 'run (process-status proc))) + (interrupt-process proc))))) + +(defun mc-pgp-fetch-from-http (id) + (let (buf connection) + (unwind-protect + (progn + (message "Fetching %s via HTTP to %s..." + (or (cdr id) (car id)) mc-pgp-keyserver-address) + (setq buf (generate-new-buffer " *mailcrypt temp*")) + (setq connection + (open-network-stream "*key fetch*" buf mc-pgp-keyserver-address + mc-pgp-keyserver-port)) + (process-send-string + connection + (concat "GET " (format mc-pgp-keyserver-url-template + (or (cdr id) (car id))) "\r\n")) + (while (and (eq 'open (process-status connection)) + (accept-process-output connection mc-pgp-fetch-timeout))) + (mc-pgp-buffer-get-key buf)) + (if buf (kill-buffer buf)) + (if connection (delete-process connection))))) + +(defun mc-pgp-fetch-from-finger (id) + (let (buf connection user host) + (unwind-protect + (and (car id) + (string-match "^\\(.+\\)@\\([^@]+\\)$" (car id)) + (progn + (message "Trying finger %s..." (car id)) + (setq user (substring (car id) + (match-beginning 1) (match-end 1))) + (setq host (substring (car id) + (match-beginning 2) (match-end 2))) + (setq buf (generate-new-buffer " *mailcrypt temp*")) + (condition-case nil + (progn + (setq connection + (open-network-stream "*key fetch*" buf host 79)) + (process-send-string connection + (concat "/W " user "\r\n")) + (while + (and (eq 'open (process-status connection)) + (accept-process-output connection + mc-pgp-fetch-timeout))) + (mc-pgp-buffer-get-key buf)) + (error nil)))) + (if buf (kill-buffer buf)) + (if connection (delete-process connection))))) + +(defvar mc-pgp-fetch-methods '(mc-pgp-fetch-from-keyrings + mc-pgp-fetch-from-finger + mc-pgp-fetch-from-http) + "List of methods to try when attempting to fetch a key. Each +element is a function to call with an ID as argument. See the +documentation for the function mc-pgp-fetch-key for a description of +the ID.") + +;;;###autoload +(defun mc-pgp-fetch-key (&optional id) + "Attempt to fetch a key for addition to PGP keyring. Interactively, +prompt for string matching key to fetch. + +Non-interactively, ID must be a pair. The CAR must be a bare Email +address and the CDR a keyID (with \"0x\" prefix). Either, but not +both, may be nil. + +Return t if we think we were successful; nil otherwise. Note that nil +is not necessarily an error, since we may have merely fired off an Email +request for the key." + (interactive) + (let ((methods mc-pgp-fetch-methods) + (process-connection-type nil) key proc buf args) + (if (null id) + (setq id (cons (read-string "Fetch key for: ") nil))) + (while (and (not key) methods) + (setq key (funcall (car methods) id)) + (setq methods (cdr methods))) + (if (not (stringp key)) + (progn + (message "Key not found.") + nil) + ;; Maybe I'll do this right someday. + (unwind-protect + (save-window-excursion + (setq buf (generate-new-buffer " *PGP Key Info*")) + (pop-to-buffer buf) + (if (< (window-height) (/ (frame-height) 2)) + (enlarge-window (- (/ (frame-height) 2) + (window-height)))) + (setq args '("-f" "+verbose=0" "+batchmode")) + (if mc-pgp-alternate-keyring + (setq args + (append args (list (format "+pubring=%s" + mc-pgp-alternate-keyring))))) + + (setq proc (apply 'start-process "*PGP*" buf mc-pgp-path args)) + ;; Because PGPPASSFD might be set + (process-send-string proc "\r\n") + (process-send-string proc key) + (process-send-string proc "\r\n") + (process-send-eof proc) + (set-buffer buf) + (while (eq 'run (process-status proc)) + (accept-process-output proc 5) + (goto-char (point-min))) + (if (y-or-n-p "Add this key to keyring? ") + (progn + (setq args (append args '("-ka"))) + (setq proc + (apply 'start-process "*PGP*" buf mc-pgp-path args)) + ;; Because PGPPASSFD might be set + (process-send-string proc "\r\n") + (process-send-string proc key) + (process-send-string proc "\r\n") + (process-send-eof proc) + (while (eq 'run (process-status proc)) + (accept-process-output proc 5)) + t))) + (if buf (kill-buffer buf)))))) + +;;}}}