Revision: 8140
          http://svn.sourceforge.net/mailman/?rev=8140&view=rev
Author:   bwarsaw
Date:     2007-01-13 19:24:31 -0800 (Sat, 13 Jan 2007)

Log Message:
-----------
Passwords done right.

First off, there are several password hashing schemes added including SHA,
salted-SHA, and RFC 2989 PBKDF2 (contributed by Bob Fleck).  Then we encode
the password using RFC 2307 style syntax.  At least I think: specifically
things like the PRF and iteration count for PBKDF2 are encoded the way I
/think/ is intended for RFC 2307 but I could be wrong.  Seems darn hard to
find definitive information about that.

In any event, even though CLEARTEXT passwords are supported, they are mostly
deprecated, even for user passwords.  It also allows us to easily update all
passwords to a new hashing scheme when the existing schemes get cracked.  The
default scheme (specified in Defaults.py.in) is salted-SHA with a 20 byte salt
(the salt length and PBKDF2 iteration counts can only be specified in the
passwords.py file).

These hashed passwords are used for user passwords, list owner and moderator
passwords, and site and list creator passwords.

Of course this means that user password reminders are impossible now.  They've
been ripped out of the code for a while, but now we'll need to implement
password resets since user passwords cannot be recovered.

bin/export has had several changes:

- export no longer converts to dollar strings.  Were assuming dollar strings
  are used by default for all new lists and any imported lists will already be
  converted to dollar strings.
- Likewise, rip out the password scheme stuff, since cleartext passwords can
  never be exported, so we might as well always include the member's hashed
  password.
- Fix exporting to stdout when that stream can only handle ascii by wrapping
  stdout in a utf-8 codec writer.

Other changes:

- add a missing import to HTTPRunner.py

- Convert GUIBase.py to use Defaults.* for constants instead of mm_cfg.*

- Remove pre-Python 2.4 compatibility from Utils.py.  We've already said
  Python 2.4 will be a minimum requirement.

- Change the permissions on the global password file.  The default 007 umask
  is used and should be good enough.

- bin/newlist adds the ability to specify the password scheme (or list the
  available schemes) for the list owner password.  It is not possible to set
  the scheme on a per-list basis.  bin/mmsitepass does the same, but for the
  site and list creator passwords.

- Fix a nasty problem with bin/import.  The comment in the code says it best:

                # XXX Here's what sucks.  Some properties need to have
                # _setValue() called on the gui component, because those
                # methods do some pre-processing on the values before they're
                # applied to the MailList instance.  But we don't have a good
                # way to find a category and sub-category that a particular
                # property belongs to.  Plus this will probably change.  So
                # for now, we'll just hard code the extra post-processing
                # here.  The good news is that not all _setValue() munging
                # needs to be done -- for example, we've already converted
                # everything to dollar strings.

- Set the 'debug' logger to logging.DEBUG level.  It doesn't seem to make much
  sense for the debugging log to ignore debug messages.

Modified Paths:
--------------
    trunk/mailman/ACKNOWLEDGMENTS.txt
    trunk/mailman/Mailman/Cgi/admin.py
    trunk/mailman/Mailman/Cgi/create.py
    trunk/mailman/Mailman/Cgi/options.py
    trunk/mailman/Mailman/Defaults.py.in
    trunk/mailman/Mailman/Gui/ContentFilter.py
    trunk/mailman/Mailman/Gui/GUIBase.py
    trunk/mailman/Mailman/OldStyleMemberships.py
    trunk/mailman/Mailman/Queue/HTTPRunner.py
    trunk/mailman/Mailman/SecurityManager.py
    trunk/mailman/Mailman/Utils.py
    trunk/mailman/Mailman/bin/export.py
    trunk/mailman/Mailman/bin/import.py
    trunk/mailman/Mailman/bin/mmsitepass.py
    trunk/mailman/Mailman/bin/newlist.py
    trunk/mailman/Mailman/loginit.py

Added Paths:
-----------
    trunk/mailman/Mailman/passwords.py

Modified: trunk/mailman/ACKNOWLEDGMENTS.txt
===================================================================
--- trunk/mailman/ACKNOWLEDGMENTS.txt   2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/ACKNOWLEDGMENTS.txt   2007-01-14 03:24:31 UTC (rev 8140)
@@ -94,6 +94,7 @@
     Kerem Erkan
     Fil
     Patrick Finnerty
+    Bob Fleck
     Erik Forsberg
     Darrell Fuhriman
     Robert Garrig\xF3s

Modified: trunk/mailman/Mailman/Cgi/admin.py
===================================================================
--- trunk/mailman/Mailman/Cgi/admin.py  2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/Cgi/admin.py  2007-01-14 03:24:31 UTC (rev 8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -29,11 +29,12 @@
 from string import lowercase, digits
 
 from Mailman import Errors
-from Mailman import i18n
 from Mailman import MailList
 from Mailman import MemberAdaptor
+from Mailman import Utils
+from Mailman import i18n
 from Mailman import mm_cfg
-from Mailman import Utils
+from Mailman import passwords
 
 from Mailman.Cgi import Auth
 from Mailman.htmlformat import *
@@ -1225,7 +1226,8 @@
     confirm = cgidata.getvalue('confirmmodpw', '').strip()
     if new or confirm:
         if new == confirm:
-            mlist.mod_password = sha.new(new).hexdigest()
+            mlist.mod_password = passwords.make_secret(
+                new, config.PASSWORD_SCHEME)
             # No re-authentication necessary because the moderator's
             # password doesn't get you into these pages.
         else:
@@ -1235,7 +1237,7 @@
     confirm = cgidata.getvalue('confirmpw', '').strip()
     if new or confirm:
         if new == confirm:
-            mlist.password = sha.new(new).hexdigest()
+            mlist.password = passwords.make_secret(new, config.PASSWORD_SCHEME)
             # Set new cookie
             print mlist.MakeCookie(mm_cfg.AuthListAdmin)
         else:

Modified: trunk/mailman/Mailman/Cgi/create.py
===================================================================
--- trunk/mailman/Mailman/Cgi/create.py 2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/Cgi/create.py 2007-01-14 03:24:31 UTC (rev 8140)
@@ -26,6 +26,7 @@
 from Mailman import MailList
 from Mailman import Message
 from Mailman import i18n
+from Mailman import passwords
 from Mailman.configuration import config
 from Mailman.htmlformat import *
 
@@ -160,7 +161,7 @@
     # We've got all the data we need, so go ahead and try to create the list
     mlist = MailList.MailList()
     try:
-        pw = sha.new(password).hexdigest()
+        pw = passwords(password, config.PASSWORD_SCHEME)
         try:
             mlist.Create(fqdn_listname, owner, pw, langs)
         except Errors.EmailAddressError, s:

Modified: trunk/mailman/Mailman/Cgi/options.py
===================================================================
--- trunk/mailman/Mailman/Cgi/options.py        2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/Cgi/options.py        2007-01-14 03:24:31 UTC (rev 
8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -24,14 +24,15 @@
 import logging
 
 from Mailman import Errors
-from Mailman import i18n
 from Mailman import MailList
 from Mailman import MemberAdaptor
+from Mailman import Utils
+from Mailman import i18n
 from Mailman import mm_cfg
-from Mailman import Utils
-
+from Mailman import passwords
 from Mailman.htmlformat import *
 
+
 OR = '|'
 SLASH = '/'
 SETLANGUAGE = -1
@@ -434,8 +435,9 @@
         if pw_globally:
             mlists.extend(lists_of_member(mlist, user))
 
+        pw = passwords.make_secret(newpw, config.PASSWORD_SCHEME)
         for gmlist in mlists:
-            change_password(gmlist, user, newpw, confirmpw)
+            change_password(gmlist, user, pw)
 
         # Regenerate the cookie so a re-authorization isn't necessary
         print mlist.MakeCookie(mm_cfg.AuthUser, user)
@@ -907,7 +909,7 @@
 
 
 
-def change_password(mlist, user, newpw, confirmpw):
+def change_password(mlist, user, newpw):
     # Must own the list lock!
     mlist.Lock()
     try:

Modified: trunk/mailman/Mailman/Defaults.py.in
===================================================================
--- trunk/mailman/Mailman/Defaults.py.in        2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/Defaults.py.in        2007-01-14 03:24:31 UTC (rev 
8140)
@@ -94,7 +94,11 @@
 # name of the temporary file that the program should operate on.
 HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s'
 
+# Default password hashing scheme.  See 'bin/mmsitepass -P' for a list of
+# available schemes.
+PASSWORD_SCHEME = 'ssha'
 
+
 
 #####
 # Database options

Modified: trunk/mailman/Mailman/Gui/ContentFilter.py
===================================================================
--- trunk/mailman/Mailman/Gui/ContentFilter.py  2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/Gui/ContentFilter.py  2007-01-14 03:24:31 UTC (rev 
8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2005 by the Free Software Foundation, Inc.
+# Copyright (C) 2002-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License

Modified: trunk/mailman/Mailman/Gui/GUIBase.py
===================================================================
--- trunk/mailman/Mailman/Gui/GUIBase.py        2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/Gui/GUIBase.py        2007-01-14 03:24:31 UTC (rev 
8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 2002-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -19,9 +19,9 @@
 
 import re
 
-from Mailman import mm_cfg
+from Mailman import Defaults
+from Mailman import Errors
 from Mailman import Utils
-from Mailman import Errors
 from Mailman.i18n import _
 
 NL = '\n'
@@ -38,17 +38,18 @@
         # Coerce and validate the new value.
         #
         # Radio buttons and boolean toggles both have integral type
-        if wtype in (mm_cfg.Radio, mm_cfg.Toggle):
+        if wtype in (Defaults.Radio, Defaults.Toggle):
             # Let ValueErrors propagate
             return int(val)
         # String and Text widgets both just return their values verbatim
-        if wtype in (mm_cfg.String, mm_cfg.Text):
+        if wtype in (Defaults.String, Defaults.Text):
             return val
         # This widget contains a single email address
-        if wtype == mm_cfg.Email:
+        if wtype == Defaults.Email:
             # BAW: We must allow blank values otherwise reply_to_address can't
-            # be cleared.  This is currently the only mm_cfg.Email type widget
-            # in the interface, so watch out if we ever add any new ones.
+            # be cleared.  This is currently the only Defaults.Email type
+            # widget in the interface, so watch out if we ever add any new
+            # ones.
             if val:
                 # Let MMBadEmailError and MMHostileAddress propagate
                 Utils.ValidateEmail(val)
@@ -56,7 +57,7 @@
         # These widget types contain lists of email addresses, one per line.
         # The EmailListEx allows each line to contain either an email address
         # or a regular expression
-        if wtype in (mm_cfg.EmailList, mm_cfg.EmailListEx):
+        if wtype in (Defaults.EmailList, Defaults.EmailListEx):
             # BAW: value might already be a list, if this is coming from
             # config_list input.  Sigh.
             if isinstance(val, list):
@@ -72,7 +73,7 @@
                 except Errors.EmailAddressError:
                     # See if this is a context that accepts regular
                     # expressions, and that the re is legal
-                    if wtype == mm_cfg.EmailListEx and addr.startswith('^'):
+                    if wtype == Defaults.EmailListEx and addr.startswith('^'):
                         try:
                             re.compile(addr)
                         except re.error:
@@ -82,10 +83,10 @@
                 addrs.append(addr)
             return addrs
         # This is a host name, i.e. verbatim
-        if wtype == mm_cfg.Host:
+        if wtype == Defaults.Host:
             return val
         # This is a number, either a float or an integer
-        if wtype == mm_cfg.Number:
+        if wtype == Defaults.Number:
             num = -1
             try:
                 num = int(val)
@@ -96,19 +97,19 @@
                 return getattr(mlist, property)
             return num
         # This widget is a select box, i.e. verbatim
-        if wtype == mm_cfg.Select:
+        if wtype == Defaults.Select:
             return val
         # Checkboxes return a list of the selected items, even if only one is
         # selected.
-        if wtype == mm_cfg.Checkbox:
+        if wtype == Defaults.Checkbox:
             if isinstance(val, list):
                 return val
             return [val]
-        if wtype == mm_cfg.FileUpload:
+        if wtype == Defaults.FileUpload:
             return val
-        if wtype == mm_cfg.Topics:
+        if wtype == Defaults.Topics:
             return val
-        if wtype == mm_cfg.HeaderFilter:
+        if wtype == Defaults.HeaderFilter:
             return val
         # Should never get here
         assert 0, 'Bad gui widget type: %s' % wtype

Modified: trunk/mailman/Mailman/OldStyleMemberships.py
===================================================================
--- trunk/mailman/Mailman/OldStyleMemberships.py        2007-01-08 04:13:20 UTC 
(rev 8139)
+++ trunk/mailman/Mailman/OldStyleMemberships.py        2007-01-14 03:24:31 UTC 
(rev 8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 2001-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 2001-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -27,9 +27,10 @@
 import time
 
 from Mailman import Errors
-from Mailman import mm_cfg
 from Mailman import MemberAdaptor
 from Mailman import Utils
+from Mailman import mm_cfg
+from Mailman import passwords
 
 ISREGULAR = 1
 ISDIGEST = 2
@@ -89,8 +90,8 @@
     def isMember(self, member):
         cpaddr, where = self.__get_cp_member(member)
         if cpaddr is not None:
-            return 1
-        return 0
+            return True
+        return False
 
     def getMemberKey(self, member):
         cpaddr, where = self.__get_cp_member(member)
@@ -115,9 +116,9 @@
 
     def authenticateMember(self, member, response):
         secret = self.getMemberPassword(member)
-        if secret == response:
+        if passwords.check_response(secret, response):
             return secret
-        return 0
+        return False
 
     def __assertIsMember(self, member):
         if not self.isMember(member):

Modified: trunk/mailman/Mailman/Queue/HTTPRunner.py
===================================================================
--- trunk/mailman/Mailman/Queue/HTTPRunner.py   2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/Queue/HTTPRunner.py   2007-01-14 03:24:31 UTC (rev 
8140)
@@ -18,6 +18,7 @@
 """Mailman HTTP runner (server)."""
 
 import sys
+import signal
 import logging
 
 from cStringIO import StringIO

Modified: trunk/mailman/Mailman/SecurityManager.py
===================================================================
--- trunk/mailman/Mailman/SecurityManager.py    2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/SecurityManager.py    2007-01-14 03:24:31 UTC (rev 
8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -61,6 +61,7 @@
 from Mailman import Defaults
 from Mailman import Errors
 from Mailman import Utils
+from Mailman import passwords
 from Mailman.configuration import config
 
 log = logging.getLogger('mailman.error')
@@ -94,7 +95,7 @@
         if authcontext == Defaults.AuthUser:
             if user is None:
                 # A bad system error
-                raise TypeError, 'No user supplied for AuthUser context'
+                raise TypeError('No user supplied for AuthUser context')
             secret = self.getMemberPassword(user)
             userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
             key += 'user+%s' % userdata
@@ -131,7 +132,7 @@
         # response, or UnAuthorized.
         for ac in authcontexts:
             if ac == Defaults.AuthCreator:
-                ok = Utils.check_global_password(response, siteadmin=0)
+                ok = Utils.check_global_password(response, siteadmin=False)
                 if ok:
                     return Defaults.AuthCreator
             elif ac == Defaults.AuthSiteAdmin:
@@ -146,13 +147,12 @@
                 key, secret = self.AuthContextInfo(ac)
                 if secret is None:
                     continue
-                sharesponse = sha.new(response).hexdigest()
-                if sharesponse == secret:
+                if passwords.check_response(secret, response):
                     return ac
             elif ac == Defaults.AuthListModerator:
                 # The list moderator password must be sha'd
                 key, secret = self.AuthContextInfo(ac)
-                if secret and sha.new(response).hexdigest() == secret:
+                if secret and passwords.check_response(secret, response):
                     return ac
             elif ac == Defaults.AuthUser:
                 if user is not None:

Modified: trunk/mailman/Mailman/Utils.py
===================================================================
--- trunk/mailman/Mailman/Utils.py      2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/Utils.py      2007-01-14 03:24:31 UTC (rev 8140)
@@ -41,16 +41,10 @@
 
 from Mailman import Errors
 from Mailman import database
+from Mailman import passwords
 from Mailman.SafeDict import SafeDict
 from Mailman.configuration import config
 
-# REMOVEME when Python 2.4 is minimum requirement
-try:
-    set
-except NameError:
-    from sets import Set as set
-
-
 AT = '@'
 CR = '\r'
 DOT = '.'
@@ -341,20 +335,16 @@
 
 
 
-def set_global_password(pw, siteadmin=True):
+def set_global_password(pw, siteadmin=True, scheme='ssha'):
     if siteadmin:
         filename = config.SITE_PW_FILE
     else:
         filename = config.LISTCREATOR_PW_FILE
-    # rw-r-----
-    # XXX Is the default umask of 007 good enough?
-    omask = os.umask(026)
     try:
         fp = open(filename, 'w')
-        fp.write(sha.new(pw).hexdigest() + '\n')
-        fp.close()
+        print >> fp, passwords.make_secret(pw, scheme)
     finally:
-        os.umask(omask)
+        fp.close()
 
 
 def get_global_password(siteadmin=True):
@@ -367,8 +357,9 @@
         challenge = fp.read()[:-1]                # strip off trailing nl
         fp.close()
     except IOError, e:
-        if e.errno <> errno.ENOENT: raise
-        # It's okay not to have a site admin password, just return false
+        if e.errno <> errno.ENOENT:
+            raise
+        # It's okay not to have a site admin password
         return None
     return challenge
 
@@ -377,7 +368,7 @@
     challenge = get_global_password(siteadmin)
     if challenge is None:
         return False
-    return challenge == sha.new(response).hexdigest()
+    return passwords.check_response(challenge, response)
 
 
 

Modified: trunk/mailman/Mailman/bin/export.py
===================================================================
--- trunk/mailman/Mailman/bin/export.py 2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/bin/export.py 2007-01-14 03:24:31 UTC (rev 8140)
@@ -18,8 +18,9 @@
 """Export an XML representation of a mailing list."""
 
 import os
-import sys
+import re
 import sha
+import sys
 import base64
 import codecs
 import datetime
@@ -35,16 +36,11 @@
 from Mailman.MailList import MailList
 from Mailman.configuration import config
 from Mailman.i18n import _
+from Mailman.initialize import initialize
 
 __i18n_templates__ = True
 
 SPACE           = ' '
-DOLLAR_STRINGS  = ('msg_header', 'msg_footer',
-                   'digest_header', 'digest_footer',
-                   'autoresponse_postings_text',
-                   'autoresponse_admin_text',
-                   'autoresponse_request_text')
-SALT_LENGTH     = 4 # bytes
 
 TYPES = {
     Defaults.Toggle         : 'bool',
@@ -148,7 +144,6 @@
             print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name)
 
     def _do_list_categories(self, mlist, k, subcat=None):
-        is_converted = bool(getattr(mlist, 'use_dollar_strings', False))
         info = mlist.GetConfigInfo(k, subcat)
         label, gui = mlist.GetConfigCategories()[k]
         if info is None:
@@ -167,12 +162,6 @@
                 value = gui.getValue(mlist, vtype, varname, data[2])
             if value is None:
                 value = getattr(mlist, varname)
-            # Do %-string to $-string conversions if the list hasn't already
-            # been converted.
-            if varname == 'use_dollar_strings':
-                continue
-            if not is_converted and varname in DOLLAR_STRINGS:
-                value = Utils.to_dollar(value)
             widget_type = TYPES[vtype]
             if isinstance(value, list):
                 self._push_element('option', name=varname, type=widget_type)
@@ -182,7 +171,7 @@
             else:
                 self._element('option', value, name=varname, type=widget_type)
 
-    def _dump_list(self, mlist, password_scheme):
+    def _dump_list(self, mlist):
         # Write list configuration values
         self._push_element('list', name=mlist.fqdn_listname)
         self._push_element('configuration')
@@ -207,8 +196,7 @@
                 attrs['original'] = cased
             self._push_element('member', **attrs)
             self._element('realname', mlist.getMemberName(member))
-            self._element('password',
-                          password_scheme(mlist.getMemberPassword(member)))
+            self._element('password', mlist.getMemberPassword(member))
             self._element('language', mlist.getMemberLanguage(member))
             # Delivery status, combined with the type of delivery
             attrs = {}
@@ -252,7 +240,7 @@
         self._pop_element('roster')
         self._pop_element('list')
 
-    def dump(self, listnames, password_scheme):
+    def dump(self, listnames):
         print >> self._fp, '<?xml version="1.0" encoding="UTF-8"?>'
         self._push_element('mailman', **{
             'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
@@ -264,7 +252,7 @@
             except Errors.MMUnknownListError:
                 print >> sys.stderr, _('No such list: $listname')
                 continue
-            self._dump_list(mlist, password_scheme)
+            self._dump_list(mlist)
         self._pop_element('mailman')
 
     def close(self):
@@ -273,41 +261,6 @@
 
 
 
-def no_password(password):
-    return '{NONE}'
-
-
-def plaintext_password(password):
-    return '{PLAIN}' + password
-
-
-def sha_password(password):
-    h = sha.new(password)
-    return '{SHA}' + base64.b64encode(h.digest())
-
-
-def ssha_password(password):
-    salt = os.urandom(SALT_LENGTH)
-    h = sha.new(password)
-    h.update(salt)
-    return '{SSHA}' + base64.b64encode(h.digest() + salt)
-
-
-SCHEMES = {
-    'none'  : no_password,
-    'plain' : plaintext_password,
-    'sha'   : sha_password,
-    }
-
-try:
-    os.urandom(1)
-except NotImplementedError:
-    pass
-else:
-    SCHEMES['ssha'] = ssha_password
-
-
-
 def parseargs():
     parser = optparse.OptionParser(version=Version.MAILMAN_VERSION,
                                    usage=_("""\
@@ -319,15 +272,6 @@
                       help=_("""\
 Output XML to FILENAME.  If not given, or if FILENAME is '-', standard out is
 used."""))
-    parser.add_option('-p', '--password-scheme',
-                      default='none', type='string', help=_("""\
-Specify the RFC 2307 style hashing scheme for passwords included in the
-output.  Use -P to get a list of supported schemes, which are
-case-insensitive."""))
-    parser.add_option('-P', '--list-hash-schemes',
-                      default=False, action='store_true', help=_("""\
-List the supported password hashing schemes and exit.  The scheme labels are
-case-insensitive."""))
     parser.add_option('-l', '--listname',
                       default=[], action='append', type='string',
                       metavar='LISTNAME', dest='listnames', help=_("""\
@@ -339,26 +283,21 @@
     if args:
         parser.print_help()
         parser.error(_('Unexpected arguments'))
-    if opts.list_hash_schemes:
-        for label in SCHEMES:
-            print label.upper()
-        sys.exit(0)
-    if opts.password_scheme.lower() not in SCHEMES:
-        parser.error(_('Invalid password scheme'))
     return parser, opts, args
 
 
 
 def main():
     parser, opts, args = parseargs()
-    config.load(opts.config)
+    initialize(opts.config)
 
+    close = False
     if opts.outputfile in (None, '-'):
-        # This will fail if there are characters in the output incompatible
-        # with system encoding.
-        fp = sys.stdout
+        writer = codecs.getwriter('utf-8')
+        fp = writer(sys.stdout)
     else:
         fp = codecs.open(opts.outputfile, 'w', 'utf-8')
+        close = True
 
     try:
         dumper = XMLDumper(fp)
@@ -370,8 +309,8 @@
                 listnames.append(listname)
         else:
             listnames = Utils.list_names()
-        dumper.dump(listnames, SCHEMES[opts.password_scheme])
+        dumper.dump(listnames)
         dumper.close()
     finally:
-        if fp is not sys.stdout:
+        if close:
             fp.close()

Modified: trunk/mailman/Mailman/bin/import.py
===================================================================
--- trunk/mailman/Mailman/bin/import.py 2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/bin/import.py 2007-01-14 03:24:31 UTC (rev 8140)
@@ -34,11 +34,9 @@
 from Mailman.i18n import _
 from Mailman.initialize import initialize
 
-
 __i18n_templates__ = True
 
 
-
 
 def nodetext(node):
     # Expect only one TEXT_NODE in the list of children
@@ -211,6 +209,20 @@
         mlist.Lock()
         try:
             for option, value in list_config.items():
+                # XXX Here's what sucks.  Some properties need to have
+                # _setValue() called on the gui component, because those
+                # methods do some pre-processing on the values before they're
+                # applied to the MailList instance.  But we don't have a good
+                # way to find a category and sub-category that a particular
+                # property belongs to.  Plus this will probably change.  So
+                # for now, we'll just hard code the extra post-processing
+                # here.  The good news is that not all _setValue() munging
+                # needs to be done -- for example, we've already converted
+                # everything to dollar strings.
+                if option in ('filter_mime_types', 'pass_mime_types',
+                              'filter_filename_extensions',
+                              'pass_filename_extensions'):
+                    value = value.splitlines()
                 setattr(mlist, option, value)
             for member in list_roster:
                 mid = member['id']

Modified: trunk/mailman/Mailman/bin/mmsitepass.py
===================================================================
--- trunk/mailman/Mailman/bin/mmsitepass.py     2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/bin/mmsitepass.py     2007-01-14 03:24:31 UTC (rev 
8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -21,8 +21,10 @@
 
 from Mailman import Utils
 from Mailman import Version
+from Mailman import passwords
 from Mailman.configuration import config
 from Mailman.i18n import _
+from Mailman.initialize import initialize
 
 __i18n_templates__ = True
 
@@ -49,20 +51,34 @@
 Set the list creator password instead of the site password.  The list
 creator is authorized to create and remove lists, but does not have
 the total power of the site administrator."""))
+    parser.add_option('-p', '--password-scheme',
+                      default=config.PASSWORD_SCHEME, type='string',
+                      help=_("""\
+Specify the RFC 2307 style hashing scheme for passwords included in the
+output.  Use -P to get a list of supported schemes, which are
+case-insensitive."""))
+    parser.add_option('-P', '--list-hash-schemes',
+                      default=False, action='store_true', help=_("""\
+List the supported password hashing schemes and exit.  The scheme labels are
+case-insensitive."""))
     parser.add_option('-C', '--config',
                       help=_('Alternative configuration file to use'))
     opts, args = parser.parse_args()
     if len(args) > 1:
-        parser.print_help()
-        print >> sys.stderr, _('Unexpected arguments')
-        sys.exit(1)
+        parser.error(_('Unexpected arguments'))
+    if opts.list_hash_schemes:
+        for label in passwords.SCHEMES:
+            print label.upper()
+        sys.exit(0)
+    if opts.password_scheme.lower() not in passwords.SCHEMES:
+        parser.error(_('Invalid password scheme'))
     return parser, opts, args
 
 
 
 def main():
     parser, opts, args = parseargs()
-    config.load(opts.config)
+    initialize(opts.config)
     if args:
         password = args[0]
     else:
@@ -77,7 +93,8 @@
             print _('Passwords do not match; no changes made.')
             sys.exit(1)
         password = pw1
-    Utils.set_global_password(password, not opts.listcreator)
+    Utils.set_global_password(password,
+                              not opts.listcreator, opts.password_scheme)
     if Utils.check_global_password(password, not opts.listcreator):
         print _('Password changed.')
     else:

Modified: trunk/mailman/Mailman/bin/newlist.py
===================================================================
--- trunk/mailman/Mailman/bin/newlist.py        2007-01-08 04:13:20 UTC (rev 
8139)
+++ trunk/mailman/Mailman/bin/newlist.py        2007-01-14 03:24:31 UTC (rev 
8140)
@@ -26,6 +26,7 @@
 from Mailman import Utils
 from Mailman import Version
 from Mailman import i18n
+from Mailman import passwords
 from Mailman.configuration import config
 from Mailman.initialize import initialize
 
@@ -72,9 +73,24 @@
 still sends the notification.  It can be used to make newlist totally
 non-interactive but still send the notification, assuming listname,
 listadmin-addr and admin-password are all specified on the command line."""))
+    parser.add_option('-p', '--password-scheme',
+                      default='ssha', type='string', help=_("""\
+Specify the RFC 2307 style hashing scheme for passwords included in the
+output.  Use -P to get a list of supported schemes, which are
+case-insensitive."""))
+    parser.add_option('-P', '--list-hash-schemes',
+                      default=False, action='store_true', help=_("""\
+List the supported password hashing schemes and exit.  The scheme labels are
+case-insensitive."""))
     parser.add_option('-C', '--config',
                       help=_('Alternative configuration file to use'))
     opts, args = parser.parse_args()
+    if opts.list_hash_schemes:
+        for label in passwords.SCHEMES:
+            print label.upper()
+        sys.exit(0)
+    if opts.password_scheme.lower() not in passwords.SCHEMES:
+        parser.error(_('Invalid password scheme'))
     # Can't verify opts.language here because the configuration isn't loaded
     # yet.
     return parser, opts, args
@@ -146,7 +162,7 @@
     # set available_languages.
     mlist.preferred_language = opts.language
     try:
-        pw = sha.new(listpasswd).hexdigest()
+        pw = passwords.make_secret(listpasswd, config.PASSWORD_SCHEME)
         try:
             mlist.Create(fqdn_listname, owner_mail, pw)
         except Errors.BadListNameError, s:

Modified: trunk/mailman/Mailman/loginit.py
===================================================================
--- trunk/mailman/Mailman/loginit.py    2007-01-08 04:13:20 UTC (rev 8139)
+++ trunk/mailman/Mailman/loginit.py    2007-01-14 03:24:31 UTC (rev 8140)
@@ -1,4 +1,4 @@
-# Copyright (C) 2006 by the Free Software Foundation, Inc.
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -92,6 +92,7 @@
     #
     # The current set of Mailman logs are:
     #
+    # debug         - Only used for development
     # error         - All exceptions go to this log
     # bounce        - All bounce processing logs go here
     # mischief      - Various types of hostile activity
@@ -105,9 +106,6 @@
     # qrunner       - qrunner start/stops
     # fromusenet    - Information related to the Usenet to Mailman gateway
     #
-    # There was also a 'debug' logger, but that was mostly unused, so instead
-    # we'll use debug level on existing loggers.
-    #
     # Start by creating a common formatter and the root logger.
     formatter = logging.Formatter(fmt=FMT, datefmt=DATEFMT)
     log = logging.getLogger('mailman')
@@ -125,6 +123,10 @@
         _handlers.append(handler)
         handler.setFormatter(formatter)
         log.addHandler(handler)
+        # It doesn't make much sense for the debug logger to ignore debug
+        # level messages.
+        if logger == 'debug':
+            log.setLevel(logging.DEBUG)
 
 
 

Added: trunk/mailman/Mailman/passwords.py
===================================================================
--- trunk/mailman/Mailman/passwords.py                          (rev 0)
+++ trunk/mailman/Mailman/passwords.py  2007-01-14 03:24:31 UTC (rev 8140)
@@ -0,0 +1,182 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Password hashing and verification schemes.
+
+Represents passwords using RFC 2307 syntax.
+"""
+
+import os
+import re
+import sha
+import hmac
+
+from array import array
+from base64 import urlsafe_b64decode as decode
+from base64 import urlsafe_b64encode as encode
+
+SALT_LENGTH = 20 # bytes
+ITERATIONS  = 2000
+
+
+
+class PasswordScheme(object):
+    @staticmethod
+    def make_secret(password):
+        """Return the hashed password""" 
+        raise NotImplementedError
+
+    @staticmethod
+    def check_response(challenge, response):
+        """Return True if response matches challenge.
+
+        It is expected that the scheme specifier prefix is already stripped
+        from the response string.
+        """
+        raise NotImplementedError
+
+
+
+class NoPasswordScheme(PasswordScheme):
+    @staticmethod
+    def make_secret(password):
+        return '{NONE}'
+
+    @staticmethod
+    def check_response(challenge, response):
+        return False
+
+
+
+class ClearTextPasswordScheme(PasswordScheme):
+    @staticmethod
+    def make_secret(password):
+        return '{CLEARTEXT}' + password
+
+    @staticmethod
+    def check_response(challenge, response):
+        return challenge == response
+
+
+
+class SHAPasswordScheme(PasswordScheme):
+    @staticmethod
+    def make_secret(password):
+        h = sha.new(password)
+        return '{SHA}' + encode(h.digest())
+
+    @staticmethod
+    def check_response(challenge, response):
+        h = sha.new(response)
+        return challenge == encode(h.digest())
+
+
+
+class SSHAPasswordScheme(PasswordScheme):
+    @staticmethod
+    def make_secret(password):
+        salt = os.urandom(SALT_LENGTH)
+        h = sha.new(password)
+        h.update(salt)
+        return '{SSHA}' + encode(h.digest() + salt)
+
+    @staticmethod
+    def check_response(challenge, response):
+        # Get the salt from the challenge
+        challenge_bytes = decode(challenge)
+        digest = challenge_bytes[:20]
+        salt = challenge_bytes[20:]
+        h = sha.new(response)
+        h.update(salt)
+        return digest == h.digest()
+
+
+
+# Given by Bob Fleck
+class PBKDF2PasswordScheme(PasswordScheme):
+    @staticmethod
+    def _pbkdf2(password, salt, iterations):
+        """From RFC2898 sec. 5.2.  Simplified to handle only 20 byte output
+        case.  Output of 20 bytes means always exactly one block to handle,
+        and a constant block counter appended to the salt in the initial hmac
+        update.
+        """
+        h = hmac.new(password, None, sha)
+        prf = h.copy()
+        prf.update(salt + '\x00\x00\x00\x01')
+        T = U = array('l', prf.digest())
+        while iterations:
+            prf = h.copy()
+            prf.update(U.tostring())
+            U = array('l', prf.digest())
+            T = array('l', (t ^ u for t, u in zip(T, U)))
+            iterations -= 1
+        return T.tostring()
+
+    @staticmethod
+    def make_secret(password):
+        """From RFC2898 sec. 5.2.  Simplified to handle only 20 byte output
+        case.  Output of 20 bytes means always exactly one block to handle,
+        and a constant block counter appended to the salt in the initial hmac
+        update.
+        """
+        salt = os.urandom(SALT_LENGTH)
+        digest = PBKDF2PasswordScheme._pbkdf2(password, salt, ITERATIONS)
+        derived_key = encode(digest + salt)
+        return '{PBKDF2 SHA %d}' % ITERATIONS + derived_key
+
+    @staticmethod
+    def check_response(challenge, response, prf, iterations):
+        # Decode the challenge to get the number of iterations and salt
+        # XXX we don't support anything but sha prf
+        if prf.lower() <> 'sha':
+            return False
+        try:
+            iterations = int(iterations)
+        except (ValueError, TypeError):
+            return False
+        challenge_bytes = decode(challenge)
+        digest = challenge_bytes[:20]
+        salt = challenge_bytes[20:]
+        key = PBKDF2PasswordScheme._pbkdf2(response, salt, iterations)
+        return digest == key
+
+
+
+SCHEMES = {
+    'none'      : NoPasswordScheme,
+    'cleartext' : ClearTextPasswordScheme,
+    'sha'       : SHAPasswordScheme,
+    'ssha'      : SSHAPasswordScheme,
+    'pbkdf2'    : PBKDF2PasswordScheme,
+    }
+
+
+def make_secret(password, scheme):
+    scheme_class = SCHEMES.get(scheme.lower(), NoPasswordScheme)
+    return scheme_class.make_secret(password)
+
+
+def check_response(challenge, response):
+    mo = re.match(r'{(?P<scheme>[^}]+?)}(?P<rest>.*)',
+                  challenge, re.IGNORECASE)
+    if not mo:
+        return False
+    scheme, rest = mo.group('scheme', 'rest')
+    scheme_parts = scheme.split()
+    scheme_class = SCHEMES.get(scheme_parts[0].lower(), NoPasswordScheme)
+    return scheme_class.check_response(rest, response, *scheme_parts[1:])


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.
_______________________________________________
Mailman-checkins mailing list
[email protected]
Unsubscribe: 
http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to