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