From mboxrd@z Thu Jan 1 00:00:00 1970 X-Msuck: nntp://news.gmane.io/gmane.emacs.gnus.general/83821 Path: news.gmane.org!not-for-mail From: Albert Krewinkel Newsgroups: gmane.emacs.gnus.general Subject: [PATCH 1/2] sasl.el: simplify authentication with SASL, make code more lispy Date: Sat, 26 Oct 2013 12:54:40 +0200 Message-ID: <1382784881-16736-2-git-send-email-tarleb@moltkeplatz.de> References: <1382784881-16736-1-git-send-email-tarleb@moltkeplatz.de> NNTP-Posting-Host: plane.gmane.org X-Trace: ger.gmane.org 1382802139 15064 80.91.229.3 (26 Oct 2013 15:42:19 GMT) X-Complaints-To: usenet@ger.gmane.org NNTP-Posting-Date: Sat, 26 Oct 2013 15:42:19 +0000 (UTC) Cc: Albert Krewinkel To: ding@gnus.org Original-X-From: ding-owner+M32077@lists.math.uh.edu Sat Oct 26 17:42:20 2013 Return-path: Envelope-to: ding-account@gmane.org Original-Received: from util0.math.uh.edu ([129.7.128.18]) by plane.gmane.org with esmtp (Exim 4.69) (envelope-from ) id 1Va60J-0004LB-Q6 for ding-account@gmane.org; Sat, 26 Oct 2013 17:42:20 +0200 Original-Received: from localhost ([127.0.0.1] helo=lists.math.uh.edu) by util0.math.uh.edu with smtp (Exim 4.63) (envelope-from ) id 1Va60A-0001pi-Bk; Sat, 26 Oct 2013 10:42:10 -0500 Original-Received: from mx2.math.uh.edu ([129.7.128.33]) by util0.math.uh.edu with esmtps (TLSv1:AES256-SHA:256) (Exim 4.63) (envelope-from ) id 1Va1Wj-0000fx-6r for ding@lists.math.uh.edu; Sat, 26 Oct 2013 05:55:29 -0500 Original-Received: from quimby.gnus.org ([80.91.231.51]) by mx2.math.uh.edu with esmtps (TLSv1:AES128-SHA:128) (Exim 4.76) (envelope-from ) id 1Va1WY-0008Iu-3h for ding@lists.math.uh.edu; Sat, 26 Oct 2013 05:55:29 -0500 Original-Received: from moltkeplatz.de ([85.214.95.47]) by quimby.gnus.org with esmtp (Exim 4.80) (envelope-from ) id 1Va1WW-0000aN-MY for ding@gnus.org; Sat, 26 Oct 2013 12:55:16 +0200 Original-Received: from caffelatte.fritz.box (dslb-178-003-097-186.pools.arcor-ip.net [178.3.97.186]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by moltkeplatz.de (Postfix) with ESMTPSA id 698E33200002; Sat, 26 Oct 2013 12:55:15 +0200 (CEST) X-Mailer: git-send-email 1.8.4.rc3 In-Reply-To: <1382784881-16736-1-git-send-email-tarleb@moltkeplatz.de> X-Spam-Score: -2.3 (--) List-ID: Precedence: bulk Xref: news.gmane.org gmane.emacs.gnus.general:83821 Archived-At: --- lisp/ChangeLog | 16 ++ lisp/{sasl-cram.el => sasl-cram-md5.el} | 29 ++- lisp/{sasl-digest.el => sasl-digest-md5.el} | 60 ++++-- lisp/sasl-ntlm.el | 24 +-- lisp/sasl.el | 312 ++++++++++++---------------- 5 files changed, 212 insertions(+), 229 deletions(-) rename lisp/{sasl-cram.el => sasl-cram-md5.el} (66%) rename lisp/{sasl-digest.el => sasl-digest-md5.el} (74%) diff --git a/lisp/ChangeLog b/lisp/ChangeLog index cf1d4ff..8cce6da 100644 --- a/lisp/ChangeLog +++ b/lisp/ChangeLog @@ -1,3 +1,19 @@ +2013-10-26 Albert Krewinkel + + * sasl.el (sasl-authenticate): New function for simpler inclusion of + SASL authentiction in protocols. + (define-sasl-mechanism): New method to register a SASL mechanism. + (sasl-make-client, make-sasl-client): Later replaces the former. + (sasl-mechanisms): sasl-mechanisms now combines the functionality of + sasl-mechanisms and sasl-mechanisms-alist. + (sasl-mechanism-steps): Load authentication steps lazily. + (sasl-mechanisms-alist, sasl-make-mechanism, sasl-step-data) + (sasl-step-set-data,sasl-plain-steps,sasl-login-steps): Removed. + * sasl-cram.el: Rename to 'sasl-cram-md5.el', simplify challenge + handling. + * sasl-digest.el: Rename to 'sasl-digest-md5.el, simplify challenge + handling. + 2013-10-23 Katsumi Yamaoka * mm-decode.el (mm-dissect-buffer): Revert last change. diff --git a/lisp/sasl-cram.el b/lisp/sasl-cram-md5.el similarity index 66% rename from lisp/sasl-cram.el rename to lisp/sasl-cram-md5.el index 2a132a5..11d2bab 100644 --- a/lisp/sasl-cram.el +++ b/lisp/sasl-cram-md5.el @@ -27,23 +27,18 @@ (require 'sasl) (require 'hmac-md5) -(defconst sasl-cram-md5-steps - '(ignore ;no initial response - sasl-cram-md5-response)) - -(defun sasl-cram-md5-response (client step) - (let ((passphrase - (sasl-read-passphrase - (format "CRAM-MD5 passphrase for %s: " - (sasl-client-name client))))) - (unwind-protect - (concat (sasl-client-name client) " " - (encode-hex-string - (hmac-md5 (sasl-step-data step) passphrase))) - (fillarray passphrase 0)))) - -(put 'sasl-cram 'sasl-mechanism - (sasl-make-mechanism "CRAM-MD5" sasl-cram-md5-steps)) +(define-sasl-mechanism CRAM-MD5 + (sasl-ignore ;no initial response + sasl-cram-md5-response)) + +(defun sasl-cram-md5-response (client challenge) + (concat + (sasl-client-name client) " " + (encode-hex-string + (hmac-md5 challenge + (sasl-read-passphrase + (format "CRAM-MD5 passphrase for %s: " + (sasl-client-name client))))))) (provide 'sasl-cram) diff --git a/lisp/sasl-digest.el b/lisp/sasl-digest-md5.el similarity index 74% rename from lisp/sasl-digest.el rename to lisp/sasl-digest-md5.el index 6adbf44..9f38809 100644 --- a/lisp/sasl-digest.el +++ b/lisp/sasl-digest-md5.el @@ -31,15 +31,10 @@ ;; ;; Passphrase should be longer than 16 bytes. (See RFC 2195) -;;; Commentary: (require 'sasl) (require 'hmac-md5) -(defvar sasl-digest-md5-nonce-count 1) -(defvar sasl-digest-md5-unique-id-function - sasl-unique-id-function) - (defvar sasl-digest-md5-syntax-table (let ((table (make-syntax-table))) (modify-syntax-entry ?= "." table) @@ -47,10 +42,45 @@ table) "A syntax table for parsing digest-challenge attributes.") -(defconst sasl-digest-md5-steps - '(ignore ;no initial response - sasl-digest-md5-response - ignore)) ;"" + +(defvar sasl-digest-md5-nonce-count 1) +(defvar sasl-digest-md5-unique-id-function + 'sasl-unique-id-function) +(defvar sasl-unique-id-function 'sasl-unique-id-function) + +(defvar sasl-unique-id-char nil) + +;; stolen (and renamed) from message.el +(defun sasl-unique-id-function () + ;; Don't use microseconds from (current-time), they may be unsupported. + ;; Instead we use this randomly inited counter. + (setq sasl-unique-id-char + (% (1+ (or sasl-unique-id-char (logand (random) (1- (lsh 1 20))))) + ;; (current-time) returns 16-bit ints, + ;; and 2^16*25 just fits into 4 digits i base 36. + (* 25 25))) + (let ((tm (current-time))) + (concat + (sasl-unique-id-number-base36 + (+ (car tm) + (lsh (% sasl-unique-id-char 25) 16)) 4) + (sasl-unique-id-number-base36 + (+ (nth 1 tm) + (lsh (/ sasl-unique-id-char 25) 16)) 4)))) + +(defun sasl-unique-id () + "Compute a data string which must be different each time. +It contain at least 64 bits of entropy." + (concat (funcall sasl-unique-id-function)(funcall sasl-unique-id-function))) + +(defun sasl-unique-id-number-base36 (num len) + (if (if (< len 0) + (<= num 0) + (= len 0)) + "" + (concat (sasl-unique-id-number-base36 (/ num 36) (1- len)) + (char-to-string (aref "zyxwvutsrqponmlkjihgfedcba9876543210" + (% num 36)))))) (defun sasl-digest-md5-parse-string (string) "Parse STRING and return a property list. @@ -109,9 +139,9 @@ charset algorithm cipher-opts auth-param)." ":00000000000000000000000000000000"))))))) (fillarray passphrase 0)))) -(defun sasl-digest-md5-response (client step) +(defun sasl-digest-md5-response (client challenge) (let* ((plist - (sasl-digest-md5-parse-string (sasl-step-data step))) + (sasl-digest-md5-parse-string challenge)) (realm (or (sasl-client-property client 'realm) (plist-get plist 'realm))) ;need to check @@ -149,9 +179,11 @@ charset algorithm cipher-opts auth-param)." digest-uri (plist-get plist 'authzid))))) -(put 'sasl-digest 'sasl-mechanism - (sasl-make-mechanism "DIGEST-MD5" sasl-digest-md5-steps)) +(define-sasl-mechanism DIGEST-MD5 + (sasl-ignore ;no initial response + sasl-digest-md5-response + sasl-ignore)) -(provide 'sasl-digest) +(provide 'sasl-digest-md5) ;;; sasl-digest.el ends here diff --git a/lisp/sasl-ntlm.el b/lisp/sasl-ntlm.el index 487a1d0..f16f693 100644 --- a/lisp/sasl-ntlm.el +++ b/lisp/sasl-ntlm.el @@ -33,14 +33,12 @@ (require 'sasl) (require 'ntlm) -(defconst sasl-ntlm-steps - '(ignore ;nothing to do before making - sasl-ntlm-request ;authentication request - sasl-ntlm-response) ;response to challenge - "A list of functions to be called in sequence for the NTLM -authentication steps. They are called by `sasl-next-step'.") +(define-sasl-mechanism NTLM + (sasl-ignore ;nothing to do before making + sasl-ntlm-request ;authentication request + sasl-ntlm-response)) ;response to challenge -(defun sasl-ntlm-request (client step) +(defun sasl-ntlm-request (client challenge) "SASL step function to generate a NTLM authentication request to the server. Called from `sasl-next-step'. CLIENT is a vector [mechanism user service server sasl-client-properties] @@ -48,19 +46,15 @@ STEP is a vector [ ]" (let ((user (sasl-client-name client))) (ntlm-build-auth-request user))) -(defun sasl-ntlm-response (client step) - "SASL step function to generate a NTLM response against the server -challenge stored in the 2nd element of STEP. Called from `sasl-next-step'." +(defun sasl-ntlm-response (client challenge) + "SASL step function to generate a NTLM response against the +server `challenge'." (let* ((user (sasl-client-name client)) (passphrase - (sasl-read-passphrase (format "NTLM passphrase for %s: " user))) - (challenge (sasl-step-data step))) + (sasl-read-passphrase (format "NTLM passphrase for %s: " user)))) (ntlm-build-auth-response challenge user (ntlm-get-password-hashes passphrase)))) -(put 'sasl-ntlm 'sasl-mechanism - (sasl-make-mechanism "NTLM" sasl-ntlm-steps)) - (provide 'sasl-ntlm) ;;; sasl-ntlm.el ends here diff --git a/lisp/sasl.el b/lisp/sasl.el index a5efdd6..5fd94b1 100644 --- a/lisp/sasl.el +++ b/lisp/sasl.el @@ -3,6 +3,7 @@ ;; Copyright (C) 2000, 2007-2013 Free Software Foundation, Inc. ;; Author: Daiki Ueno +;; Albert Krewinkel ;; Keywords: SASL ;; This file is part of GNU Emacs. @@ -34,237 +35,182 @@ ;;; Code: -(defvar sasl-mechanisms - '("CRAM-MD5" "DIGEST-MD5" "PLAIN" "LOGIN" "ANONYMOUS" - "NTLM" "SCRAM-MD5")) +(eval-when-compile (require 'cl)) +(require 'hmac-md5) -(defvar sasl-mechanism-alist - '(("CRAM-MD5" sasl-cram) - ("DIGEST-MD5" sasl-digest) - ("PLAIN" sasl-plain) - ("LOGIN" sasl-login) - ("ANONYMOUS" sasl-anonymous) - ("NTLM" sasl-ntlm) - ("SCRAM-MD5" sasl-scram))) +(defvar sasl-mechanisms '((PLAIN) (LOGIN) (DIGEST-MD5) (CRAM-MD5) (NTLM)) + "Associative list of known SASL mechanisms and their respective steps. -(defvar sasl-unique-id-function #'sasl-unique-id-function) +If the list of steps of a mechanism is empty, the respective +package `sasl-mech' should be required (replace 'mech' with the +lower-case mechanism name.") + +;; SASL Error (put 'sasl-error 'error-message "SASL error") (put 'sasl-error 'error-conditions '(sasl-error error)) - (defun sasl-error (datum) (signal 'sasl-error (list datum))) + ;;; @ SASL client ;;; -(defun sasl-make-client (mechanism name service server) - "Return a newly allocated SASL client. -NAME is name of the authorization. SERVICE is name of the service desired. -SERVER is the fully qualified host name of the server to authenticate to." - (vector mechanism name service server (make-symbol "sasl-client-properties"))) - -(defun sasl-client-mechanism (client) - "Return the authentication mechanism driver of CLIENT." - (aref client 0)) - -(defun sasl-client-name (client) - "Return the authorization name of CLIENT, a string." - (aref client 1)) - -(defun sasl-client-service (client) - "Return the service name of CLIENT, a string." - (aref client 2)) - -(defun sasl-client-server (client) - "Return the server name of CLIENT, a string." - (aref client 3)) - -(defun sasl-client-set-properties (client plist) - "Destructively set the properties of CLIENT. -The second argument PLIST is the new property list." - (setplist (aref client 4) plist)) +;; A SASL client. `mechanism' and `name' are the SASL mechanism and the name +;; used for authentication, respectively. `service' is name of the service +;; desired. `server' is the fully qualified host name of the server to +;; authenticate to. Additional information, like the name used for +;; authorization, are safed in `properties' in form of a plist. +(defstruct sasl-client + mechanism + name + service + server + properties) (defun sasl-client-set-property (client property value) "Add the given PROPERTY/VALUE to CLIENT." - (put (aref client 4) property value)) + (put (sasl-client-properties client) property value)) (defun sasl-client-property (client property) "Return the value of the PROPERTY of CLIENT." - (get (aref client 4) property)) - -(defun sasl-client-properties (client) - "Return the properties of CLIENT." - (symbol-plist (aref client 4))) + (get (sasl-client-properties client) property)) ;;; @ SASL mechanism ;;; - -(defun sasl-make-mechanism (name steps) - "Make an authentication mechanism. +(defmacro define-sasl-mechanism (name steps &optional provide) + "Register an authentication mechanism, safe in `sasl-mechanisms'. NAME is a IANA registered SASL mechanism name. -STEPS is list of continuation functions." - (vector name - (mapcar - (lambda (step) - (let ((symbol (make-symbol (symbol-name step)))) - (fset symbol (symbol-function step)) - symbol)) - steps))) +STEPS is a list of continuation functions." + `(progn + (if (assoc ',name sasl-mechanisms) + (setf (cdr (assoc ',name sasl-mechanisms)) ',steps) + (add-to-list 'sasl-mechanisms (cons ',name ',steps) 'append)) + ,(when provide + `(provide (intern (concat "sasl-" (downcase (symbol-name ',name)))))))) + +(defun sasl-mechanism (mechanism-identifier) + "Get the mechanism identified by `mechanism-identifier'." + (assoc (if (stringp mechanism-identifier) + (intern (upcase mechanism-identifier)) + mechanism-identifier) + sasl-mechanisms)) (defun sasl-mechanism-name (mechanism) "Return name of MECHANISM, a string." - (aref mechanism 0)) + (symbol-name (car mechanism))) (defun sasl-mechanism-steps (mechanism) "Return the authentication steps of MECHANISM, a list of functions." - (aref mechanism 1)) - -(defun sasl-find-mechanism (mechanisms) - "Retrieve an appropriate mechanism object from MECHANISMS hints." - (let* ((sasl-mechanisms sasl-mechanisms) - (mechanism - (catch 'done - (while sasl-mechanisms - (if (member (car sasl-mechanisms) mechanisms) - (throw 'done (nth 1 (assoc (car sasl-mechanisms) - sasl-mechanism-alist)))) - (setq sasl-mechanisms (cdr sasl-mechanisms)))))) - (if mechanism - (require mechanism)) - (get mechanism 'sasl-mechanism))) - -;;; @ SASL authentication step -;;; + (require + (intern (concat "sasl-" (downcase (sasl-mechanism-name mechanism))))) + (cdr mechanism)) -(defun sasl-step-data (step) - "Return the data which STEP holds, a string." - (aref step 1)) - -(defun sasl-step-set-data (step data) - "Store DATA string to STEP." - (aset step 1 data)) - -(defun sasl-next-step (client step) - "Evaluate the challenge and prepare an appropriate next response. -The data type of the value and 2nd argument STEP is nil or opaque -authentication step which holds the reference to the next action and -the current challenge. At the first time STEP should be set to nil." - (let* ((steps - (sasl-mechanism-steps - (sasl-client-mechanism client))) - (function - (if (vectorp step) - (nth 1 (memq (aref step 0) steps)) - (car steps)))) - (if function - (vector function (funcall function client step))))) +(defun sasl-supported-mechanisms (acceptable) + "Return the intersection of supported and `acceptable' SASL mechanisms." + (remove nil (mapcar 'sasl-mechanism acceptable))) +(defun sasl-find-mechanism (preferred-mechanisms) + "Retrieve the best available from a list of `preferred-mechanisms'." + (let ((mechanism (car (sasl-supported-mechanisms preferred-mechanisms)))) + mechanism)) + + +;;; Utility Functions +;;; (defvar sasl-read-passphrase nil) (defun sasl-read-passphrase (prompt) - (if (not sasl-read-passphrase) - (if (functionp 'read-passwd) - (setq sasl-read-passphrase 'read-passwd) - (if (load "passwd" t) - (setq sasl-read-passphrase 'read-passwd) - (autoload 'ange-ftp-read-passwd "ange-ftp") - (setq sasl-read-passphrase 'ange-ftp-read-passwd)))) - (funcall sasl-read-passphrase prompt)) - -(defun sasl-unique-id () - "Compute a data string which must be different each time. -It contain at least 64 bits of entropy." - (concat (funcall sasl-unique-id-function)(funcall sasl-unique-id-function))) - -(defvar sasl-unique-id-char nil) - -;; stolen (and renamed) from message.el -(defun sasl-unique-id-function () - ;; Don't use microseconds from (current-time), they may be unsupported. - ;; Instead we use this randomly inited counter. - (setq sasl-unique-id-char - (% (1+ (or sasl-unique-id-char (logand (random) (1- (lsh 1 20))))) - ;; (current-time) returns 16-bit ints, - ;; and 2^16*25 just fits into 4 digits i base 36. - (* 25 25))) - (let ((tm (current-time))) - (concat - (sasl-unique-id-number-base36 - (+ (car tm) - (lsh (% sasl-unique-id-char 25) 16)) 4) - (sasl-unique-id-number-base36 - (+ (nth 1 tm) - (lsh (/ sasl-unique-id-char 25) 16)) 4)))) - -(defun sasl-unique-id-number-base36 (num len) - (if (if (< len 0) - (<= num 0) - (= len 0)) - "" - (concat (sasl-unique-id-number-base36 (/ num 36) (1- len)) - (char-to-string (aref "zyxwvutsrqponmlkjihgfedcba9876543210" - (% num 36)))))) + (funcall (or sasl-read-passphrase 'passwd) prompt)) + +(defun sasl-chain-steps (client steps chaining-fn &optional initial-challenge) + "Chain the SASL mechanism `steps' together using `chaining-fn'." + (let ((challenge initial-challenge)) + (dolist (step steps) + (print step) + (setf challenge (funcall chaining-fn (funcall step client challenge)))))) + +(defun* sasl-authenticate (name password mechanisms server service &rest spec + &key send-response + send-initial-response + receive-challenge + &allow-other-keys) + "Authenticate `name' against `server' for `service'. +The first SASL mechanism, which is both supported and in +`mechanisms', is used to authenticate the user. The list +`mechanisms' should therefor be ordered by preference (highest +preference first). + +The keyword arguments all are functions specifying the protocol +specific parts of a SASL exchange. `send-initial-response' takes +a `sasl-client' and the initial response and sends it to the +server. `send-response' has the same arguments list, but is used +for consequential responses. `receive-challenge' is an +argument-less function returning the servers challenge/result. + +Error handling is the responsibility of the supplied functions +and/or the calling function." + (let* ((mech (sasl-find-mechanism mechanisms)) + (client (make-sasl-client :mechanism mech :name name + :service service + :server server + :properties nil)) + (steps (sasl-mechanism-steps mech)) + (sasl-read-passphrase (when password `(lambda (x) ,password)))) + (let ((chaining-function + (lambda (response) + (funcall receive-challenge + (funcall send-response client response))))) + (sasl-chain-steps client (cdr steps) chaining-function + (funcall send-initial-response client + (funcall (car steps) client nil))) + (funcall receive-challenge)))) + ;;; PLAIN (RFC2595 Section 6) -(defconst sasl-plain-steps - '(sasl-plain-response)) - -(defun sasl-plain-response (client step) - (let ((passphrase - (sasl-read-passphrase - (format "PLAIN passphrase for %s: " (sasl-client-name client)))) - (authenticator-name - (sasl-client-property - client 'authenticator-name)) - (name (sasl-client-name client))) - (unwind-protect - (if (and authenticator-name - (not (string= authenticator-name name))) - (concat authenticator-name "\0" name "\0" passphrase) - (concat "\0" name "\0" passphrase)) - (fillarray passphrase 0)))) - -(put 'sasl-plain 'sasl-mechanism - (sasl-make-mechanism "PLAIN" sasl-plain-steps)) - -(provide 'sasl-plain) +;;; +(defun sasl-plain-response (client challenge) + (let ((passphrase-fn + (lambda () + (sasl-read-passphrase + (format "PLAIN passphrase for %s: " (sasl-client-name client)))))) + (let ((authenticator-name (sasl-client-property client 'authenticator-name)) + (name (sasl-client-name client))) + (if (and authenticator-name + (not (string= authenticator-name name))) + (concat authenticator-name "\0" name "\0" (funcall passphrase-fn)) + (concat "\0" name "\0" (funcall passphrase-fn)))))) + +(define-sasl-mechanism PLAIN + (sasl-plain-response) + t) + ;;; LOGIN (No specification exists) -(defconst sasl-login-steps - '(ignore ;no initial response - sasl-login-response-1 - sasl-login-response-2)) - -(defun sasl-login-response-1 (client step) -;;; (unless (string-match "^Username:" (sasl-step-data step)) -;;; (sasl-error (format "Unexpected response: %s" (sasl-step-data step)))) +;;; +(defun sasl-login-response-1 (client challenge) (sasl-client-name client)) (defun sasl-login-response-2 (client step) -;;; (unless (string-match "^Password:" (sasl-step-data step)) -;;; (sasl-error (format "Unexpected response: %s" (sasl-step-data step)))) (sasl-read-passphrase (format "LOGIN passphrase for %s: " (sasl-client-name client)))) -(put 'sasl-login 'sasl-mechanism - (sasl-make-mechanism "LOGIN" sasl-login-steps)) +(define-sasl-mechanism LOGIN + (sasl-ignore + sasl-login-response-1 + sasl-login-response-2) + t) -(provide 'sasl-login) ;;; ANONYMOUS (RFC2245) -(defconst sasl-anonymous-steps - '(ignore ;no initial response - sasl-anonymous-response)) - +;;; (defun sasl-anonymous-response (client step) (or (sasl-client-property client 'trace) (sasl-client-name client))) -(put 'sasl-anonymous 'sasl-mechanism - (sasl-make-mechanism "ANONYMOUS" sasl-anonymous-steps)) - -(provide 'sasl-anonymous) +(define-sasl-mechanism ANONYMOUS + (sasl-ignore + sasl-anonymous-response) + t) (provide 'sasl) - ;;; sasl.el ends here -- 1.8.4.rc3