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