Importing Oort Gnus v0.05.
[elisp/gnus.git-] / lisp / canlock.el
1 ;;; canlock.el --- functions for Cancel-Lock feature
2
3 ;; Copyright (C) 1998, 1999, 2001, 2002 Free Software Foundation, Inc.
4
5 ;; Author: Katsumi Yamaoka <yamaoka@jpl.org>
6 ;; Keywords: news, cancel-lock, hmac, sha1, rfc2104
7
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
11 ;; any later version.
12
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program; see the file COPYING.  If not, write to the
20 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21 ;; Boston, MA 02111-1307, USA.
22
23 ;;; Commentary:
24
25 ;; Canlock is a library for generating and verifying Cancel-Lock and/or
26 ;; Cancel-Key header in news articles.  This is used to protect articles
27 ;; from rogue cancel, supersede or replace attacks.  The method is based
28 ;; on draft-ietf-usefor-cancel-lock-01.txt which was released on November
29 ;; 3rd 1998.  For instance, you can add Cancel-Lock (and possibly Cancel-
30 ;; Key) header in a news article by using a hook which will be evaluated
31 ;; just before sending an article as follows:
32 ;;
33 ;; (add-hook '*e**a*e-header-hook 'canlock-insert-header t)
34 ;;
35 ;; Verifying Cancel-Lock is mainly a function of news servers, however,
36 ;; you can verify your own article using the command `canlock-verify' in
37 ;; the (raw) article buffer.  You will be prompted for the password for
38 ;; each time if the option `canlock-password' or `canlock-password-for-
39 ;; verify' is nil.  Note that setting these options is a bit unsafe.
40
41 ;;; Code:
42
43 (defconst canlock-version "0.8")
44
45 (eval-when-compile
46   (require 'cl))
47
48 (autoload 'sha1-binary "sha1-el")
49
50 (defgroup canlock nil
51   "The Cancel-Lock feature."
52   :group 'applications)
53
54 (defcustom canlock-sha1-function 'sha1-binary
55   "Function to call to make a SHA-1 message digest."
56   :type '(radio (function-item sha1-binary)
57                 (function-item canlock-sha1-with-openssl)
58                 (function :tag "Other"))
59   :group 'canlock)
60
61 (defcustom canlock-sha1-function-for-verify canlock-sha1-function
62   "Function to call to make a SHA-1 message digest for verifying."
63   :type '(radio (function-item sha1-binary)
64                 (function-item canlock-sha1-with-openssl)
65                 (function :tag "Other"))
66   :group 'canlock)
67
68 (defcustom canlock-openssl-program "openssl"
69   "Name of OpenSSL program."
70   :type 'string
71   :group 'canlock)
72
73 (defcustom canlock-openssl-args '("sha1")
74   "Arguments passed to the OpenSSL program."
75   :type 'sexp
76   :group 'canlock)
77
78 (defcustom canlock-ignore-errors nil
79   "If non-nil, ignore any error signals."
80   :type 'boolean
81   :group 'canlock)
82
83 (defcustom canlock-password nil
84   "Password to use when signing a Cancel-Lock or a Cancel-Key header."
85   :type 'string
86   :group 'canlock)
87
88 (defcustom canlock-password-for-verify canlock-password
89   "Password to use when verifying a Cancel-Lock or a Cancel-Key header."
90   :type 'string
91   :group 'canlock)
92
93 (defcustom canlock-force-insert-header nil
94   "If non-nil, insert a Cancel-Lock or a Cancel-Key header even if the
95 buffer does not look like a news message."
96   :type 'boolean
97   :group 'canlock)
98
99 (eval-when-compile
100   (defmacro canlock-string-as-unibyte (string)
101     "Return a unibyte string with the same individual bytes as STRING."
102     (if (fboundp 'string-as-unibyte)
103         (list 'string-as-unibyte string)
104       string)))
105
106 (defun canlock-sha1-with-openssl (message)
107   "Make a SHA-1 digest of MESSAGE using OpenSSL."
108   (let (default-enable-multibyte-characters)
109     (with-temp-buffer
110       (let ((coding-system-for-read 'binary)
111             (coding-system-for-write 'binary)
112             selective-display
113             (case-fold-search t))
114         (insert message)
115         (apply 'call-process-region (point-min) (point-max)
116                canlock-openssl-program t t nil canlock-openssl-args)
117         (goto-char (point-min))
118         (insert "\"")
119         (while (re-search-forward "[0-9a-f][0-9a-f]" nil t)
120           (replace-match (concat "\\\\x" (match-string 0))))
121         (insert "\"")
122         (goto-char (point-min))
123         (canlock-string-as-unibyte (read (current-buffer)))))))
124
125 (defvar canlock-read-passwd nil)
126 (defun canlock-read-passwd (prompt &rest args)
127   "Read a password using PROMPT.
128 If ARGS, PROMPT is used as an argument to `format'."
129   (let ((prompt
130          (if args
131              (apply 'format prompt args)
132            prompt)))
133     (unless canlock-read-passwd
134       (if (or (fboundp 'read-passwd) (load "passwd" t))
135           (setq canlock-read-passwd 'read-passwd)
136         (unless (fboundp 'ange-ftp-read-passwd)
137           (autoload 'ange-ftp-read-passwd "ange-ftp"))
138         (setq canlock-read-passwd 'ange-ftp-read-passwd)))
139     (funcall canlock-read-passwd prompt)))
140
141 (defun canlock-make-cancel-key (message-id password)
142   "Make a Cancel-Key header."
143   (cond ((> (length password) 20)
144          (setq password (funcall canlock-sha1-function password)))
145         ((< (length password) 20)
146          (setq password (concat
147                          password
148                          (make-string (- 20 (length password)) 0)))))
149   (setq password (concat password (make-string 44 0)))
150   (let ((ipad (mapconcat (lambda (char)
151                            (char-to-string (logxor 54 char)))
152                          password ""))
153         (opad (mapconcat (lambda (char)
154                            (char-to-string (logxor 92 char)))
155                          password "")))
156     (base64-encode-string
157      (funcall canlock-sha1-function
158               (concat
159                opad
160                (funcall canlock-sha1-function
161                         (concat ipad
162                                 (canlock-string-as-unibyte message-id))))))))
163
164 (defun canlock-narrow-to-header ()
165   "Narrow the buffer to the head of the message."
166   (let (case-fold-search)
167     (narrow-to-region
168      (goto-char (point-min))
169      (goto-char (if (re-search-forward
170                      (format "^$\\|^%s$"
171                              (regexp-quote mail-header-separator))
172                      nil t)
173                     (match-beginning 0)
174                   (point-max))))))
175
176 (defun canlock-delete-headers ()
177   "Delete Cancel-Key or Cancel-Lock headers in the narrowed buffer."
178   (let ((case-fold-search t))
179     (goto-char (point-min))
180     (while (re-search-forward "^Cancel-\\(Key\\|Lock\\):" nil t)
181       (delete-region (match-beginning 0)
182                      (if (re-search-forward "^[^\t ]" nil t)
183                          (goto-char (match-beginning 0))
184                        (point-max))))))
185
186 (defun canlock-fetch-fields (&optional key)
187   "Return a list of the values of Cancel-Lock header.
188 If KEY is non-nil, look for a Cancel-Key header instead.  The buffer
189 is expected to be narrowed to just the headers of the message."
190   (let ((field (mail-fetch-field (if key "Cancel-Key" "Cancel-Lock")))
191         fields rest
192         (case-fold-search t))
193     (when field
194       (setq fields (split-string field "[\t\n\r ,]+"))
195       (while fields
196         (when (string-match "^sha1:" (setq field (pop fields)))
197           (push (substring field 5) rest)))
198       (nreverse rest))))
199
200 (defun canlock-fetch-id-for-key ()
201   "Return a Message-ID in Cancel, Supersedes or Replaces header.
202 The buffer is expected to be narrowed to just the headers of the
203 message."
204   (or (let ((cancel (mail-fetch-field "Control")))
205         (and cancel
206              (string-match "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)"
207                            cancel)
208              (match-string 1 cancel)))
209       (mail-fetch-field "Supersedes")
210       (mail-fetch-field "Replaces")))
211
212 ;;;###autoload
213 (defun canlock-insert-header (&optional id-for-key id-for-lock password)
214   "Insert a Cancel-Key and/or a Cancel-Lock header if possible."
215   (let (news control key-for-key key-for-lock)
216     (save-excursion
217       (save-restriction
218         (canlock-narrow-to-header)
219         (when (setq news (or canlock-force-insert-header
220                              (mail-fetch-field "Newsgroups")))
221           (unless id-for-key
222             (setq id-for-key (canlock-fetch-id-for-key)))
223           (if (and (setq control (mail-fetch-field "Control"))
224                    (string-match
225                     "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)"
226                     control))
227               (setq id-for-lock nil)
228             (unless id-for-lock
229               (setq id-for-lock (mail-fetch-field "Message-ID"))))
230           (canlock-delete-headers)
231           (goto-char (point-max))))
232       (when news
233         (if (not (or id-for-key id-for-lock))
234             (message "There are no Message-ID(s)")
235           (unless password
236             (setq password (or canlock-password
237                                (canlock-read-passwd
238                                 "Password for Canlock: "))))
239           (if (or (not (stringp password)) (zerop (length password)))
240               (message "Password for Canlock is bad")
241             (setq key-for-key (when id-for-key
242                                 (canlock-make-cancel-key
243                                  id-for-key password))
244                   key-for-lock (when id-for-lock
245                                  (canlock-make-cancel-key
246                                   id-for-lock password)))
247             (if (not (or key-for-key key-for-lock))
248                 (message "Couldn't insert Canlock header")
249               (when key-for-key
250                 (insert "Cancel-Key: sha1:" key-for-key "\n"))
251               (when key-for-lock
252                 (insert "Cancel-Lock: sha1:"
253                         (base64-encode-string (funcall canlock-sha1-function
254                                                        key-for-lock))
255                         "\n")))))))))
256
257 ;;;###autoload
258 (defun canlock-verify (&optional buffer)
259   "Verify Cancel-Lock or Cancel-Key in BUFFER.
260 If BUFFER is nil, the current buffer is assumed.  Signal an error if
261 it fails.  You can modify the behavior of this function to return non-
262 nil instead of to signal an error by setting the option
263 `canlock-ignore-errors' to non-nil."
264   (interactive)
265   (let ((canlock-sha1-function (or canlock-sha1-function-for-verify
266                                    canlock-sha1-function))
267         keys locks errmsg id-for-key id-for-lock password
268         key-for-key key-for-lock match)
269     (save-excursion
270       (when buffer
271         (set-buffer buffer))
272       (save-restriction
273         (widen)
274         (canlock-narrow-to-header)
275         (setq keys (canlock-fetch-fields 'key)
276               locks (canlock-fetch-fields))
277         (if (not (or keys locks))
278             (setq errmsg
279                   "There are neither Cancel-Lock nor Cancel-Key headers")
280           (setq id-for-key (canlock-fetch-id-for-key)
281                 id-for-lock (mail-fetch-field "Message-ID"))
282           (or id-for-key id-for-lock
283               (setq errmsg "There are no Message-ID(s)")))))
284
285     (if errmsg
286         (if canlock-ignore-errors
287             errmsg
288           (error "%s" errmsg))
289
290       (setq password (or canlock-password-for-verify
291                          (canlock-read-passwd "Password for Canlock: ")))
292       (if (or (not (stringp password)) (zerop (length password)))
293           (progn
294             (setq errmsg "Password for Canlock is bad")
295             (if canlock-ignore-errors
296                 errmsg
297               (error "%s" errmsg)))
298
299         (when keys
300           (when id-for-key
301             (setq key-for-key (canlock-make-cancel-key id-for-key password))
302             (while (and keys (not match))
303               (setq match (string-equal key-for-key (pop keys)))))
304           (setq keys (if match "good" "bad")))
305         (setq match nil)
306
307         (when locks
308           (when id-for-lock
309             (setq key-for-lock
310                   (base64-encode-string (funcall canlock-sha1-function
311                                                  (canlock-make-cancel-key
312                                                   id-for-lock password))))
313             (when (and locks (not match))
314               (setq match (string-equal key-for-lock (pop locks)))))
315           (setq locks (if match "good" "bad")))
316
317         (prog1
318             (when (member "bad" (list keys locks))
319               "bad")
320           (cond ((and keys locks)
321                  (message "Cancel-Key is %s, Cancel-Lock is %s" keys locks))
322                 (locks
323                  (message "Cancel-Lock is %s" locks))
324                 (keys
325                  (message "Cancel-Key is %s" keys))))))))
326
327 (provide 'canlock)
328
329 ;;; canlock.el ends here