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