diff options
| author | bwarsaw | 2007-01-14 03:24:31 +0000 |
|---|---|---|
| committer | bwarsaw | 2007-01-14 03:24:31 +0000 |
| commit | 898eeaebe945b82c270a31578dabd19728f4475b (patch) | |
| tree | 7c85252428206288b9df74075ea8b15097dfb390 | |
| parent | 758c067131369c35b4b737d409ca7809dcd4920a (diff) | |
| download | mailman-898eeaebe945b82c270a31578dabd19728f4475b.tar.gz mailman-898eeaebe945b82c270a31578dabd19728f4475b.tar.zst mailman-898eeaebe945b82c270a31578dabd19728f4475b.zip | |
| -rw-r--r-- | ACKNOWLEDGMENTS.txt | 1 | ||||
| -rw-r--r-- | Mailman/Cgi/admin.py | 12 | ||||
| -rw-r--r-- | Mailman/Cgi/create.py | 3 | ||||
| -rw-r--r-- | Mailman/Cgi/options.py | 14 | ||||
| -rw-r--r-- | Mailman/Defaults.py.in | 4 | ||||
| -rw-r--r-- | Mailman/Gui/ContentFilter.py | 2 | ||||
| -rw-r--r-- | Mailman/Gui/GUIBase.py | 35 | ||||
| -rw-r--r-- | Mailman/OldStyleMemberships.py | 13 | ||||
| -rw-r--r-- | Mailman/Queue/HTTPRunner.py | 1 | ||||
| -rw-r--r-- | Mailman/SecurityManager.py | 12 | ||||
| -rw-r--r-- | Mailman/Utils.py | 25 | ||||
| -rw-r--r-- | Mailman/bin/export.py | 89 | ||||
| -rw-r--r-- | Mailman/bin/import.py | 16 | ||||
| -rw-r--r-- | Mailman/bin/mmsitepass.py | 29 | ||||
| -rw-r--r-- | Mailman/bin/newlist.py | 18 | ||||
| -rw-r--r-- | Mailman/loginit.py | 10 | ||||
| -rw-r--r-- | Mailman/passwords.py | 182 |
17 files changed, 319 insertions, 147 deletions
diff --git a/ACKNOWLEDGMENTS.txt b/ACKNOWLEDGMENTS.txt index 984f22898..29717e30f 100644 --- a/ACKNOWLEDGMENTS.txt +++ b/ACKNOWLEDGMENTS.txt @@ -94,6 +94,7 @@ in answering questions on mailman-users. Kerem Erkan Fil Patrick Finnerty + Bob Fleck Erik Forsberg Darrell Fuhriman Robert Garrigós diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py index d0da502bf..6977b4592 100644 --- a/Mailman/Cgi/admin.py +++ b/Mailman/Cgi/admin.py @@ -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 email.Utils import unquote, parseaddr, formataddr from string import lowercase, digits from Mailman import Errors -from Mailman import i18n from Mailman import MailList from Mailman import MemberAdaptor -from Mailman import mm_cfg from Mailman import Utils +from Mailman import i18n +from Mailman import mm_cfg +from Mailman import passwords from Mailman.Cgi import Auth from Mailman.htmlformat import * @@ -1225,7 +1226,8 @@ def change_options(mlist, category, subcat, cgidata, doc): 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 @@ def change_options(mlist, category, subcat, cgidata, doc): 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: diff --git a/Mailman/Cgi/create.py b/Mailman/Cgi/create.py index 53001b356..0ebe7b9f6 100644 --- a/Mailman/Cgi/create.py +++ b/Mailman/Cgi/create.py @@ -26,6 +26,7 @@ from Mailman import Errors 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 @@ def process_request(doc, cgidata): # 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: diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py index 3b86aeb99..1ce8ee234 100644 --- a/Mailman/Cgi/options.py +++ b/Mailman/Cgi/options.py @@ -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 urllib import logging from Mailman import Errors -from Mailman import i18n from Mailman import MailList from Mailman import MemberAdaptor -from Mailman import mm_cfg from Mailman import Utils - +from Mailman import i18n +from Mailman import mm_cfg +from Mailman import passwords from Mailman.htmlformat import * + OR = '|' SLASH = '/' SETLANGUAGE = -1 @@ -434,8 +435,9 @@ address. Upon confirmation, any other mailing list containing the address 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 lists_of_member(mlist, user): -def change_password(mlist, user, newpw, confirmpw): +def change_password(mlist, user, newpw): # Must own the list lock! mlist.Lock() try: diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 49238b3c3..1b85bf861 100644 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -94,6 +94,10 @@ ALLOW_SITE_ADMIN_COOKIES = No # 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' + ##### diff --git a/Mailman/Gui/ContentFilter.py b/Mailman/Gui/ContentFilter.py index a2fad54b2..0744c268e 100644 --- a/Mailman/Gui/ContentFilter.py +++ b/Mailman/Gui/ContentFilter.py @@ -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 diff --git a/Mailman/Gui/GUIBase.py b/Mailman/Gui/GUIBase.py index 53cc3e490..e12ac5984 100644 --- a/Mailman/Gui/GUIBase.py +++ b/Mailman/Gui/GUIBase.py @@ -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 Utils +from Mailman import Defaults from Mailman import Errors +from Mailman import Utils from Mailman.i18n import _ NL = '\n' @@ -38,17 +38,18 @@ class GUIBase: # 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 @@ class GUIBase: # 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 @@ class GUIBase: 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 @@ class GUIBase: 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 @@ class GUIBase: 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 diff --git a/Mailman/OldStyleMemberships.py b/Mailman/OldStyleMemberships.py index 8961c2825..70f076481 100644 --- a/Mailman/OldStyleMemberships.py +++ b/Mailman/OldStyleMemberships.py @@ -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 @@ This is the adaptor used by default in Mailman 2.1. 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 @@ class OldStyleMemberships(MemberAdaptor.MemberAdaptor): 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 @@ class OldStyleMemberships(MemberAdaptor.MemberAdaptor): 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): diff --git a/Mailman/Queue/HTTPRunner.py b/Mailman/Queue/HTTPRunner.py index b4de67fd7..12b5bb607 100644 --- a/Mailman/Queue/HTTPRunner.py +++ b/Mailman/Queue/HTTPRunner.py @@ -18,6 +18,7 @@ """Mailman HTTP runner (server).""" import sys +import signal import logging from cStringIO import StringIO diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py index 6740a958f..9b7401e59 100644 --- a/Mailman/SecurityManager.py +++ b/Mailman/SecurityManager.py @@ -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 urlparse import urlparse 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 @@ class SecurityManager: 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 @@ class SecurityManager: # 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 @@ class SecurityManager: 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: diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 0d46e7d32..94a84532f 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -41,16 +41,10 @@ from string import ascii_letters, digits, whitespace 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 GetRandomSeed(): -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 @@ def get_global_password(siteadmin=True): 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 @@ def check_global_password(response, siteadmin=True): challenge = get_global_password(siteadmin) if challenge is None: return False - return challenge == sha.new(response).hexdigest() + return passwords.check_response(challenge, response) diff --git a/Mailman/bin/export.py b/Mailman/bin/export.py index d83959279..b0b5519ef 100644 --- a/Mailman/bin/export.py +++ b/Mailman/bin/export.py @@ -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 import Version 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): 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 @@ class XMLDumper(object): -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 @@ Export the configuration and members of a mailing list in XML format.""")) 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 @@ included in the XML output. Multiple -l flags may be given.""")) 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 @@ def main(): 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() diff --git a/Mailman/bin/import.py b/Mailman/bin/import.py index b643de389..e9171f73a 100644 --- a/Mailman/bin/import.py +++ b/Mailman/bin/import.py @@ -34,11 +34,9 @@ from Mailman.MailList import MailList 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 @@ def create(all_listdata): 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'] diff --git a/Mailman/bin/mmsitepass.py b/Mailman/bin/mmsitepass.py index 3bd2c77d7..0d986bf6a 100644 --- a/Mailman/bin/mmsitepass.py +++ b/Mailman/bin/mmsitepass.py @@ -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 @@ import optparse 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 @@ If password is not given on the command line, it will be prompted for. 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 @@ def main(): 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: diff --git a/Mailman/bin/newlist.py b/Mailman/bin/newlist.py index a127b25e1..bde615974 100644 --- a/Mailman/bin/newlist.py +++ b/Mailman/bin/newlist.py @@ -26,6 +26,7 @@ from Mailman import Message 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 @@ This option suppresses the prompt prior to administrator notification but 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 @@ def main(): # 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: diff --git a/Mailman/loginit.py b/Mailman/loginit.py index 31a2860ec..70412f4da 100644 --- a/Mailman/loginit.py +++ b/Mailman/loginit.py @@ -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 @@ def initialize(propagate=False): # # 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 @@ def initialize(propagate=False): # 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 @@ def initialize(propagate=False): _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) diff --git a/Mailman/passwords.py b/Mailman/passwords.py new file mode 100644 index 000000000..30c436a01 --- /dev/null +++ b/Mailman/passwords.py @@ -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:]) |
