e8eb67c03d3449be44946844ff8fb28f696916a4
[gnus] / 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 (autoload 'base64-encode-string "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-binary)
58                 (function-item canlock-sha1-with-openssl)
59                 (function :tag "Other"))
60   :group 'canlock)
61
62 (defcustom canlock-sha1-function-for-verify canlock-sha1-function
63   "Function to call to make a SHA-1 message digest for verifying."
64   :type '(radio (function-item sha1-binary)
65                 (function-item canlock-sha1-with-openssl)
66                 (function :tag "Other"))
67   :group 'canlock)
68
69 (defcustom canlock-openssl-program "openssl"
70   "Name of OpenSSL program."
71   :type 'string
72   :group 'canlock)
73
74 (defcustom canlock-openssl-args '("sha1")
75   "Arguments passed to the OpenSSL program."
76   :type 'sexp
77   :group 'canlock)
78
79 (defcustom canlock-ignore-errors nil
80   "If non-nil, ignore any error signals."
81   :type 'boolean
82   :group 'canlock)
83
84 (defcustom canlock-password nil
85   "Password to use when signing a Cancel-Lock or a Cancel-Key header."
86   :type 'string
87   :group 'canlock)
88
89 (defcustom canlock-password-for-verify canlock-password
90   "Password to use when verifying a Cancel-Lock or a Cancel-Key header."
91   :type 'string
92   :group 'canlock)
93
94 (defcustom canlock-force-insert-header nil
95   "If non-nil, insert a Cancel-Lock or a Cancel-Key header even if the
96 buffer does not look like a news message."
97   :type 'boolean
98   :group 'canlock)
99
100 (eval-when-compile
101   (defmacro canlock-string-as-unibyte (string)
102     "Return a unibyte string with the same individual bytes as STRING."
103     (if (fboundp 'string-as-unibyte)
104         (list 'string-as-unibyte string)
105       string)))
106
107 (defun canlock-sha1-with-openssl (message)
108   "Make a SHA-1 digest of MESSAGE using OpenSSL."
109   (let (default-enable-multibyte-characters)
110     (with-temp-buffer
111       (let ((coding-system-for-read 'binary)
112             (coding-system-for-write 'binary)
113             selective-display
114             (case-fold-search t))
115         (insert message)
116         (apply 'call-process-region (point-min) (point-max)
117                canlock-openssl-program t t nil canlock-openssl-args)
118         (goto-char (point-min))
119         (insert "\"")
120         (while (re-search-forward "[0-9a-f][0-9a-f]" nil t)
121           (replace-match (concat "\\\\x" (match-string 0))))
122         (insert "\"")
123         (goto-char (point-min))
124         (canlock-string-as-unibyte (read (current-buffer)))))))
125
126 (defvar canlock-read-passwd nil)
127 (defun canlock-read-passwd (prompt &rest args)
128   "Read a password using PROMPT.
129 If ARGS, PROMPT is used as an argument to `format'."
130   (let ((prompt
131          (if args
132              (apply 'format prompt args)
133            prompt)))
134     (unless canlock-read-passwd
135       (if (or (fboundp 'read-passwd) (load "passwd" t))
136           (setq canlock-read-passwd 'read-passwd)
137         (unless (fboundp 'ange-ftp-read-passwd)
138           (autoload 'ange-ftp-read-passwd "ange-ftp"))
139         (setq canlock-read-passwd 'ange-ftp-read-passwd)))
140     (funcall canlock-read-passwd prompt)))
141
142 (defun canlock-make-cancel-key (message-id password)
143   "Make a Cancel-Key header."
144   (cond ((> (length password) 20)
145          (setq password (funcall canlock-sha1-function password)))
146         ((< (length password) 20)
147          (setq password (concat
148                          password
149                          (make-string (- 20 (length password)) 0)))))
150   (setq password (concat password (make-string 44 0)))
151   (let ((ipad (mapconcat (lambda (char)
152                            (char-to-string (logxor 54 char)))
153                          password ""))
154         (opad (mapconcat (lambda (char)
155                            (char-to-string (logxor 92 char)))
156                          password "")))
157     (base64-encode-string
158      (canlock-string-as-unibyte
159       (funcall canlock-sha1-function
160                (concat
161                 opad
162                 (funcall canlock-sha1-function
163                          (concat ipad
164                                  (canlock-string-as-unibyte message-id)))))))))
165
166 (defun canlock-narrow-to-header ()
167   "Narrow the buffer to the head of the message."
168   (let (case-fold-search)
169     (narrow-to-region
170      (goto-char (point-min))
171      (goto-char (if (re-search-forward
172                      (format "^$\\|^%s$"
173                              (regexp-quote mail-header-separator))
174                      nil t)
175                     (match-beginning 0)
176                   (point-max))))))
177
178 (defun canlock-delete-headers ()
179   "Delete Cancel-Key or Cancel-Lock headers in the narrowed buffer."
180   (let ((case-fold-search t))
181     (goto-char (point-min))
182     (while (re-search-forward "^Cancel-\\(Key\\|Lock\\):" nil t)
183       (delete-region (match-beginning 0)
184                      (if (re-search-forward "^[^\t ]" nil t)
185                          (goto-char (match-beginning 0))
186                        (point-max))))))
187
188 (defun canlock-fetch-fields (&optional key)
189   "Return a list of the values of Cancel-Lock header.
190 If KEY is non-nil, look for a Cancel-Key header instead.  The buffer
191 is expected to be narrowed to just the headers of the message."
192   (let ((field (mail-fetch-field (if key "Cancel-Key" "Cancel-Lock")))
193         fields rest
194         (case-fold-search t))
195     (when field
196       (setq fields (split-string field "[\t\n\r ,]+"))
197       (while fields
198         (when (string-match "^sha1:" (setq field (pop fields)))
199           (push (substring field 5) rest)))
200       (nreverse rest))))
201
202 (defun canlock-fetch-id-for-key ()
203   "Return a Message-ID in Cancel, Supersedes or Replaces header.
204 The buffer is expected to be narrowed to just the headers of the
205 message."
206   (or (let ((cancel (mail-fetch-field "Control")))
207         (and cancel
208              (string-match "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)"
209                            cancel)
210              (match-string 1 cancel)))
211       (mail-fetch-field "Supersedes")
212       (mail-fetch-field "Replaces")))
213
214 ;;;###autoload
215 (defun canlock-insert-header (&optional id-for-key id-for-lock password)
216   "Insert a Cancel-Key and/or a Cancel-Lock header if possible."
217   (let (news control key-for-key key-for-lock)
218     (save-excursion
219       (save-restriction
220         (canlock-narrow-to-header)
221         (when (setq news (or canlock-force-insert-header
222                              (mail-fetch-field "Newsgroups")))
223           (unless id-for-key
224             (setq id-for-key (canlock-fetch-id-for-key)))
225           (if (and (setq control (mail-fetch-field "Control"))
226                    (string-match
227                     "^cancel[\t ]+\\(<[^\t\n @<>]+@[^\t\n @<>]+>\\)"
228                     control))
229               (setq id-for-lock nil)
230             (unless id-for-lock
231               (setq id-for-lock (mail-fetch-field "Message-ID"))))
232           (canlock-delete-headers)
233           (goto-char (point-max))))
234       (when news
235         (if (not (or id-for-key id-for-lock))
236             (message "There are no Message-ID(s)")
237           (unless password
238             (setq password (or canlock-password
239                                (canlock-read-passwd
240                                 "Password for Canlock: "))))
241           (if (or (not (stringp password)) (zerop (length password)))
242               (message "Password for Canlock is bad")
243             (setq key-for-key (when id-for-key
244                                 (canlock-make-cancel-key
245                                  id-for-key password))
246                   key-for-lock (when id-for-lock
247                                  (canlock-make-cancel-key
248                                   id-for-lock password)))
249             (if (not (or key-for-key key-for-lock))
250                 (message "Couldn't insert Canlock header")
251               (when key-for-key
252                 (insert "Cancel-Key: sha1:" key-for-key "\n"))
253               (when key-for-lock
254                 (insert "Cancel-Lock: sha1:"
255                         (base64-encode-string (funcall canlock-sha1-function
256                                                        key-for-lock))
257                         "\n")))))))))
258
259 ;;;###autoload
260 (defun canlock-verify (&optional buffer)
261   "Verify Cancel-Lock or Cancel-Key in BUFFER.
262 If BUFFER is nil, the current buffer is assumed.  Signal an error if
263 it fails.  You can modify the behavior of this function to return non-
264 nil instead of to signal an error by setting the option
265 `canlock-ignore-errors' to non-nil."
266   (interactive)
267   (let ((canlock-sha1-function (or canlock-sha1-function-for-verify
268                                    canlock-sha1-function))
269         keys locks errmsg id-for-key id-for-lock password
270         key-for-key key-for-lock match)
271     (save-excursion
272       (when buffer
273         (set-buffer buffer))
274       (save-restriction
275         (widen)
276         (canlock-narrow-to-header)
277         (setq keys (canlock-fetch-fields 'key)
278               locks (canlock-fetch-fields))
279         (if (not (or keys locks))
280             (setq errmsg
281                   "There are neither Cancel-Lock nor Cancel-Key headers")
282           (setq id-for-key (canlock-fetch-id-for-key)
283                 id-for-lock (mail-fetch-field "Message-ID"))
284           (or id-for-key id-for-lock
285               (setq errmsg "There are no Message-ID(s)")))))
286
287     (if errmsg
288         (if canlock-ignore-errors
289             errmsg
290           (error "%s" errmsg))
291
292       (setq password (or canlock-password-for-verify
293                          (canlock-read-passwd "Password for Canlock: ")))
294       (if (or (not (stringp password)) (zerop (length password)))
295           (progn
296             (setq errmsg "Password for Canlock is bad")
297             (if canlock-ignore-errors
298                 errmsg
299               (error "%s" errmsg)))
300
301         (when keys
302           (when id-for-key
303             (setq key-for-key (canlock-make-cancel-key id-for-key password))
304             (while (and keys (not match))
305               (setq match (string-equal key-for-key (pop keys)))))
306           (setq keys (if match "good" "bad")))
307         (setq match nil)
308
309         (when locks
310           (when id-for-lock
311             (setq key-for-lock
312                   (base64-encode-string (funcall canlock-sha1-function
313                                                  (canlock-make-cancel-key
314                                                   id-for-lock password))))
315             (when (and locks (not match))
316               (setq match (string-equal key-for-lock (pop locks)))))
317           (setq locks (if match "good" "bad")))
318
319         (prog1
320             (when (member "bad" (list keys locks))
321               "bad")
322           (cond ((and keys locks)
323                  (message "Cancel-Key is %s, Cancel-Lock is %s" keys locks))
324                 (locks
325                  (message "Cancel-Lock is %s" locks))
326                 (keys
327                  (message "Cancel-Key is %s" keys))))))))
328
329 (provide 'canlock)
330
331 ;;; canlock.el ends here