[ZODB-Dev] AuthZEO, cut 1

Christian Reis kiko@async.com.br
Sat, 25 Jan 2003 13:10:26 -0200


--3MwIy2ne0vdjdPXF
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline


Hey there. Johan and I hacked up a first cut of the AuthZEO patch. It's
a bit long but shouldn't be too hard to grasp, and I'll do an overview
here. We're sending this off for approval and review, though we've done
a lot of it already. The remaining issues are in the final section, so
FF to there if you don't want to get into details.

1. Authentication Modules

We've implemented three authentication mechanisms: plaintext, SHA and
SRP (named 'plaintext', 'sha' and 'srp', accordingly). None of them
store plaintext passwords on disk: the password file only stores hashes.
The plaintext variant is only called so because it sends the password in
clear over the wire.

The SRP authenticator includes a version of SRP implemented by Tom
Holyrod, implemented in the files ZEO/auth/SRP.py and ZEO/auth/hmac.py.
The original (complete) version is available at
http://members.tripod.com/professor_tom/archives/srpsocket.html 

Minor changes were done to SRP.py to conform to our requirements. We
will submit them upstream when this patch has been approved; this should
allow upgrades in these files to be done easily if there are updates to
the original package.

2. Usage

After installing the [patched] version of ZEO, the only changes required
to enable auth are:

    a. Create a password database.  We implemented a simple script
       (zpasswd.py) that creates, changes and removes users from the
       password database. It's installed into $(prefix)/bin, and offers
       basic usage information.

    b. Set up the server. If you use a standalone script, all that needs
       to be done is pass in extra parameters (auth_filename,
       auth_protocol) to StorageServer. If you use the new configuration
       mechanism and runzeo.py, use the extra options auth-filename
       and auth-protocol.

    c. Set up the client. Pass in "username=foo" and "password=bar" to
       the ClientStorage constructor. Normally you will read from the
       console or somewhere the password, and use the value read here.

       There is an issue: at the moment, if you don't use wait=0 and
       authentication fails, the client hangs. Since this is a change
       that involves zrpc and is rather isolated we will look into this
       as soon as this patch is reviewed and approved.

3. Design

The outline is: 

    a. StorageServer now upon startup initializes a special subclass of
       ZEOStorage when authentication is enabled. This subclass
       implements an authentication module (Find them in
       ZEO/auth/auth_*.py). It also initializes a single Database
       instance that abstracts access to the password file.

    b. ClientStorage requests authentication information from
       StorageServer. If authentication is enabled, it imports the
       client authenticator (also found in ZEO/auth/auth_*.py), and
       starts authentication.

    c. The client authenticator and storage authenticator process the
       authentication protocol and return (to their own sides) if
       authentication went well or not. They use extensionMethod() to
       actually communicate. This involves the server accessing the
       database instance.

    d. The server authenticator upon register() checks if authentication
       went okay, thus eliminating the chance that an old or hacked
       ClientStorage managed to skip authentication.

The design isn't too intrusive, but the database is a bit of a jinx in
it: it can't be fully abstracted and independent because, since the
passwords are stored in a protocol-dependent format, the storage auth
module needs to know what it should expect back (an sha crypted hash, a
plaintext password (we don't offer this at the moment) or an SRP hash,
validator tuple). This is why SRP uses an SRPDatabase class.

No changes have been done so far to zrpc, but see section 6 about
protocol and exception issues.

3. Walkthrough of the diff

The changes are not too complex; here's an outline:

    - ClientStorage: added username/password, testConnection() now calls
      stub.getAuthProtocol() and self.doAuth(), doAuth() doing the
      important module and Client import from auth/auth_*.py, and
      actually instantiating and start()ing Client.

    - ServerStub: added getAuthProtocol() stub.

    - ZEOStorage: getAuthProtocol() returns the protocol string name.  

    - StorageServer: accepts constructor parameters auth_protocol (a
      string name) and auth_database (a filename). It does _setup_auth()
      when authentication is used, importing the StorageClass and
      Database classes, and instantiating the last. When the server is
      instantiated, it hands it a reference to the database too (no
      other easy way to give database ref to storage).
      
    - StorageServer instantiates one of the authenticated storages
      (subclasses of AuthZEOStorage, an "abstract" class) corresponding
      to auth_protocol, and a Database instance that abstracts access to
      the password database file.

    - SRP.py and hmac.py implement SRP internals. The srp class defined
      in it is unused by us.

    - auth/storage.py defines AuthZEOStorage, which all authenticated
      storages should inherit from. It defines a specialized register()
      that checks for authentication, and finish_auth() described in the
      next section.

    - zpasswd.py is the password database maintenence script.

    - runzeo and schema.xml have only been changed to accept the new
      parameters.

    - testAuth.py is a simple suite of 6 tests for each of the
      authentication modules. It uses wait=0 and connection.poll() to
      avoid hanging on failure.

    - setup.py makes adjustments for installation.

4. Adding new modules

A new authentication module can be implemented by simply defining a new
name for the protocol (let's say X), and adding a new file called
ZEO/auth/auth_X.py. This file should contain classes that implement the
Client and StorageClass authenticators, and if necessary a Database
subclass (a two/three class change). StorageClass should inherit from
AuthZEOStorage and should call finish_auth(boolean) upon validating the
client. The protocol can happen in as many steps as necessary, in a very
simple fashion; just use separate extensionMethod()s for them. 

I'm not sure if pluggable authentication is overkill, but I think we
should take into account the fact that some people may *not* want to run
SRP for some reason (patent issues), even though it is the more secure
option. Jeremy has agreed on this.

5. Interoperability

Summary: when upgrading an installation, first upgrade all ZEO servers,
and then their clients.

An old (pre-auth) client can access a server with the auth version of ZEO
installed, as long as the server doesn't *use* authentication (by
specifying auth_protocol/auth-protocol as anything but none). This way
the server base can be upgraded to the auth version without breaking any
clients. Of course, when enabling authentication, the clients need to be
changed at the same time as the server.

However, a new (auth) client can *not* access a pre-auth ZEO server,
because it lacks the getAuthProtocol() call. The client *will* hang if
wait=1 is specified, and will fail regardless. I am unsure if this can
be handled with changing the zRPC protocol version, and would appreciate
feedback on this.

6. Remaining issues

    - Database checking: the current version only load()s the password
      file upon ZEO server startup. This is a bug, and it should reload
      the file if mtime has been updated. Since we only realized this
      now, and want to avoid to redo the QA and review that has gone
      into this patch, we'll fix this as soon as this patch has been
      approved.

    - Protocol versioning: should the zRPC protocol be updated? What are
      the effects on client and server interoperability, as discussed
      above.

    - Client-side hang on error: the client hangs when authentication
      fails. This should be fixed by changing zrpc to consider some
      failures (authentication, DNS, connectivity?) fatal and raising an
      exception in these cases. This should be an optional change to
      allow robust servers to continue enjoying the reconnection
      facility they have today.

    - Copyright/Licensing: I am still unsure if the files included from
      SRPSocket should be assigned Zope.com copyright (they are
      copyright Tom Holyrod at the moment, and I haven't asked him for a
      concession). On the license front, they have been offered under
      "BSD or whatever", in the author's words <wink>.

      Johan and I have assigned the copyright according to the standard
      in the ZEO tree to the files we have implemented.

Well, that's about it. I would really appreciate it if "somebody" can
check this into a branch so diffs can be easier to produce (not that
they are very hard, but incremental changes are hard to analyze). And
we'll be hoping for comments.

Take care,
--
Christian Reis, Senior Engineer, Async Open Source, Brazil.
http://async.com.br/~kiko/ | [+55 16] 261 2331 | NMFL

--3MwIy2ne0vdjdPXF
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="AuthZEO.diff"

diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/ClientStorage.py ZODB3-Auth/ZEO/ClientStorage.py
--- ZODB3/ZEO/ClientStorage.py	Mon Jan 20 19:03:09 2003
+++ ZODB3-Auth/ZEO/ClientStorage.py	Sat Jan 25 10:18:23 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -29,7 +29,8 @@
 from ZEO import ClientCache, ServerStub
 from ZEO.TransactionBuffer import TransactionBuffer
 from ZEO.Exceptions \
-     import ClientStorageError, UnrecognizedResult, ClientDisconnected
+     import ClientStorageError, UnrecognizedResult, ClientDisconnected, \
+            AuthError
 from ZEO.zrpc.client import ConnectionManager
 
 from ZODB import POSException
@@ -99,7 +100,8 @@
                  min_disconnect_poll=5, max_disconnect_poll=300,
                  wait_for_server_on_startup=None, # deprecated alias for wait
                  wait=None, # defaults to 1
-                 read_only=0, read_only_fallback=0):
+                 read_only=0, read_only_fallback=0,
+                 username='', password=''):
 
         """ClientStorage constructor.
 
@@ -159,6 +161,17 @@
             writable storages are available.  Defaults to false.  At
             most one of read_only and read_only_fallback should be
             true.
+
+        username -- string with username to be used when authenticating.
+            These only need to be provided if you are connecting to an
+            authenticated server storage.
+ 
+        password -- string with plaintext password to be used
+            when authenticated.
+
+        Note that the authentication protocol is defined by the server
+        and is detected by the ClientStorage upon connecting (see
+        testConnection() and doAuth() for details).
         """
 
         log2(INFO, "%s (pid=%d) created %s/%s for storage: %r" %
@@ -214,6 +227,8 @@
         self._is_read_only = read_only
         self._storage = storage
         self._read_only_fallback = read_only_fallback
+        self._username = username
+        self._password = password
         # _server_addr is used by sortKey()
         self._server_addr = None
         self._tfile = None
@@ -342,6 +357,33 @@
         if cn is not None:
             cn.pending()
 
+    def doAuth(self, protocol, stub):
+        if self._username == '' and self._password == '':
+            raise AuthError, "empty username or password"
+
+        # import the auth module
+        # XXX: Should we validate the client module that is being specified
+        # by the server? A malicious server could cause any auth_*.py file
+        # to be loaded according to Python import semantics.
+        fullname = 'ZEO.auth.auth_' + protocol
+        try:
+            module = __import__(fullname, globals(), locals(), protocol)
+        except ImportError:
+            log("%s: no such an auth protocol: %s" %
+                (self.__class__.__name__, protocol))
+
+        # instantiate the client authenticator
+        Client = getattr(module, 'Client', None)
+        if not Client:
+            log("%s: %s is not a valid auth protocol, must have a " + \
+                "Client class" % (self.__class__.__name__, protocol))
+            raise AuthError, "invalid protocol"
+        
+        c = Client(stub)
+        
+        # Initiate authentication, returns boolean specifying whether OK
+        return c.start(self._username, self._password)
+        
     def testConnection(self, conn):
         """Internal: test the given connection.
 
@@ -366,6 +408,12 @@
         log2(INFO, "Testing connection %r" % conn)
         # XXX Check the protocol version here?
         stub = self.StorageServerStubClass(conn)
+
+        # XXX: Verify return value?
+        auth = stub.getAuthProtocol()
+        if auth and not self.doAuth(auth, stub):
+            raise AuthError, "Authentication failed"
+        
         try:
             stub.register(str(self._storage), self._is_read_only)
             return 1
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/Exceptions.py ZODB3-Auth/ZEO/Exceptions.py
--- ZODB3/ZEO/Exceptions.py	Wed Jan 15 16:19:15 2003
+++ ZODB3-Auth/ZEO/Exceptions.py	Sat Jan 25 09:44:24 2003
@@ -24,3 +24,5 @@
 class ClientDisconnected(ClientStorageError):
     """The database storage is disconnected from the storage."""
 
+class AuthError(StorageError):
+    """The client provided invalid authentication credentials."""
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/ServerStub.py ZODB3-Auth/ZEO/ServerStub.py
--- ZODB3/ZEO/ServerStub.py	Tue Jan 14 17:08:33 2003
+++ ZODB3-Auth/ZEO/ServerStub.py	Sat Jan 25 09:44:24 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -45,6 +45,9 @@
     def get_info(self):
         return self.rpc.call('get_info')
 
+    def getAuthProtocol(self):
+        return self.rpc.call('getAuthProtocol')
+    
     def lastTransaction(self):
         # Not in protocol version 2.0.0; see __init__()
         return self.rpc.call('lastTransaction')
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/StorageServer.py ZODB3-Auth/ZEO/StorageServer.py
--- ZODB3/ZEO/StorageServer.py	Mon Jan 20 19:26:31 2003
+++ ZODB3-Auth/ZEO/StorageServer.py	Sat Jan 25 12:21:09 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -31,6 +31,7 @@
 
 from ZEO import ClientStub
 from ZEO.CommitLog import CommitLog
+from ZEO.auth.database import Database
 from ZEO.monitor import StorageStats, StatsServer
 from ZEO.zrpc.server import Dispatcher
 from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay
@@ -161,6 +162,8 @@
         """Select the storage that this client will use
 
         This method must be the first one called by the client.
+        For authenticated storages this method will be called by the client
+        immediately after authentication is finished.
         """
         if self.storage is not None:
             self.log("duplicate register() call")
@@ -410,6 +413,15 @@
         else:
             return self._wait(lambda: self._vote())
 
+    def getAuthProtocol(self):
+        """Return string specifying name of authentication module to use.
+
+           The module name should be auth_%s where %s is auth_protocol."""
+        protocol = self.server.auth_protocol
+        if not protocol or protocol == 'none':
+            return None
+        return protocol
+    
     def abortVersion(self, src, id):
         self._check_tid(id, exc=StorageTransactionError)
         if self.locked:
@@ -577,7 +589,9 @@
     def __init__(self, addr, storages, read_only=0,
                  invalidation_queue_size=100,
                  transaction_timeout=None,
-                 monitor_address=None):
+                 monitor_address=None,
+                 auth_protocol=None,
+                 auth_filename=None):
         """StorageServer constructor.
 
         This is typically invoked from the start.py script.
@@ -618,7 +632,22 @@
         monitor_address -- The address at which the monitor server
             should listen.  If specified, a monitor server is started.
             The monitor server provides server statistics in a simple
-            text format. 
+            text format.
+
+        auth_protocol -- The name of the authentication protocol to use.
+            Supported protocols: 'none', 'plaintext', 'sha' and 'srp'.
+            
+        auth_filename -- The name of the password database filename.
+            Note that the database should be in a format compatible with
+            the authentication protocol used; for instance, protocol
+            "srp" requires a different format.
+            
+            Note that to implement an authentication protocol, a server
+            and client authentication mechanism must be implemented in a
+            auth_* module, which should be stored inside the "auth"
+            subdirectory. This module may also define a DatabaseClass
+            variable that should indicate what database should be used
+            by the authenticator.
         """
 
         self.addr = addr
@@ -633,6 +662,10 @@
         for s in storages.values():
             s._waiting = []
         self.read_only = read_only
+        self.auth_protocol = auth_protocol
+        self.auth_filename = auth_filename
+        if auth_protocol:
+            self._setup_auth(auth_protocol)
         # A list of at most invalidation_queue_size invalidations
         self.invq = []
         self.invq_bound = invalidation_queue_size
@@ -654,7 +687,45 @@
             self.monitor = StatsServer(monitor_address, self.stats)
         else:
             self.monitor = None
+            
+    def _setup_auth(self, protocol):
+        # Load the auth module
+        fullname = 'ZEO.auth.auth_' + protocol
+        try:
+            module = __import__(fullname, globals(), locals(), protocol)
+        except ImportError:
+            log("%s: no such an auth protocol: %s" %
+                (self.__class__.__name__, protocol))
+            self.auth_protocol = None
+            return
+        
+        from ZEO.auth.storage import AuthZEOStorage
+        
+        # And set up the appropriate storage class
+        klass = getattr(module, 'StorageClass', None)
+        if not klass or not issubclass(klass, AuthZEOStorage):
+            log(("%s: %s is not a valid auth protocol, must have a " + \
+                "StorageClass class") % (self.__class__.__name__, protocol))
+            self.auth_protocol = None
+            return
+        # Save class for new_connection()
+        self.ZEOStorageClass = klass
 
+        log("%s: using auth protocol: %s" % \
+            (self.__class__.__name__, protocol))
+       
+        # Check if the auth module defines a special database
+        dbklass = getattr(module, 'DatabaseClass', None)
+        if not dbklass:
+            dbklass = Database
+
+        # We create a Database instance here for use with the authenticator
+        # modules. Having one instance allows it to be shared between multiple
+        # storages, avoiding the need to bloat each with a new authenticator
+        # Database that would contain the same info, and also avoiding any
+        # possibly synchronization issues between them.
+        self.database = dbklass(self.auth_filename)
+        
     def new_connection(self, sock, addr):
         """Internal: factory to create a new connection.
 
@@ -663,6 +734,8 @@
         connection.
         """
         z = self.ZEOStorageClass(self, self.read_only)
+        if self.auth_protocol:
+            z.set_database(self.database)
         c = self.ManagedServerConnectionClass(sock, addr, z, self)
         log("new connection %s: %s" % (addr, `c`))
         return c
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/SRP.py ZODB3-Auth/ZEO/auth/SRP.py
--- ZODB3/ZEO/auth/SRP.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/SRP.py	Sat Jan 25 10:15:02 2003
@@ -0,0 +1,291 @@
+"""Secure Remote Passwords.  This is slightly different from the standard
+implementation (with regard to the definition of 'u', the authentication
+hash, and the fact that the database is a pickle).  Also the default random
+number generator is not cryptographically strong.  It may be good enough to
+password protect your MUD, if not your bank account.  Note that the passwd
+database should not be world readable, or it will be vulnerable to a
+dictionary attack (like the standard Unix password file).  See the SRP
+distribution at http://srp.stanford.edu/srp/ for more information.
+"""
+
+import sha
+from hmac import hmac
+import random
+import getpass
+import pickle
+
+# Some constants defining the sizes of various entities.
+
+saltlen = 16    # bytes
+tlen = 128      # bits
+ablen = 128     # bits
+
+# The prime field to work in, and the base to use.  Note that this must be
+# common to both client and host. (Alternatively, the host can send these
+# values to the client, who should then verify that they are safe.)
+# The first number is a prime p of the form 2q + 1, where q is also prime.
+# The second number is a generator in the field GF(p).
+
+pflist = [(137656596376486790043182744734961384933899167257744121335064027192370741112305920493080254690601316526576747330553110881621319493425219214435734356437905637147670206858858966652975541347966997276817657605917471296442404150473520316654025988200256062845025470327802138620845134916799507318209468806715548156999L, 8623462398472349872L)]
+
+# New exceptions we raise.
+
+class NoSuchUser(Exception): pass
+class ImproperKeyValue(Exception): pass
+class AuthFailure(Exception): pass
+
+# Some utility functions:
+
+def random_long(bits):
+
+    """Generate a random long integer with the given number of bits."""
+
+    r = 0L
+    chunk = 24
+    bchunk = (1 << chunk) - 1
+    while bits > 0:
+        if bits < chunk:
+            bchunk = (1 << bits) - 1
+        i = random.randint(0, bchunk)
+        r = (r << chunk) + i
+        bits = bits - chunk
+    return r
+
+def random_string(bytes):
+
+    """Generate a random string with the given number of bytes."""
+
+    r = ''
+    for i in range(0, bytes):
+        r = r + chr(random.randint(0, 255))
+    return r
+
+def string_to_long(s):
+
+    """Convert a string of bytes into a long integer."""
+
+    r = 0L
+    for c in s:
+        r = (r << 8) + ord(c)
+    return r
+
+def long_to_string(i):
+
+    """Convert a long integer into a string of bytes."""
+
+    s = ''
+    while i > 0:
+        s = chr(i & 255) + s
+        i = i >> 8
+    return s
+
+def hash(s):
+
+    """Hash a value with some hashing algorithm."""
+
+    if type(s) != type(''):
+        s = long_to_string(s)
+
+    return sha.new(s).digest()
+
+def private_key(u, s, p):
+
+    """Given the username, salt, and cleartext password, return the private
+    key, which is the long integer form of the hashed arguments."""
+
+    h = hash(s + hash(u + p))
+    x = string_to_long(h)
+    return x
+
+# This creates a new entry for the host password database.  In other words,
+# this is called when the user changes his password.
+# Note that when this is done over the network, the channel should be
+# encrypted.  The password should obviously never be sent in the clear, and
+# neither should the salt, verifier pair, as they are vulnerable to a
+# dictionary attack.  For the same reason, the passwd database should not be
+# world readable.
+
+def create_new_verifier(u, p, pf):
+
+    """Given a username, cleartext password, and a prime field, pick a
+    random salt and calculate the verifier.  The salt, verifier tuple is
+    returned."""
+
+    s = random_string(saltlen)
+    n, g = pf
+    v = pow(g, private_key(u, s, p), n)
+    return (s, v)
+
+def new_passwd(user):
+    pfid = 0
+    pf = pflist[pfid]
+    password = getpass.getpass('Enter new password for %s: ' % user)
+    salt, verifier = create_new_verifier(user, password, pf)
+    passwd[user] = (salt, verifier, pfid)
+
+# This is the authentication protocol.  There are two parts, the client and
+# the host.  These functions are called from the client side.
+
+def client_begin(user):
+
+    # Here we could optionally query the host for the pfid and salt, or
+    # indeed the pf itself plus salt.  We'd have to verify that n and g
+    # are valid in the latter case, and we need a local copy anyway in the
+    # former.
+
+    pfid = 0
+    n, g = pflist[pfid]
+
+    # Pick a random number and send it to the host, who responds with
+    # the user's salt and more random numbers.  Note that in the standard
+    # SRP implementation, u is derived from B.
+
+    a = random_long(ablen)
+    A = pow(g, a, n)
+
+    return (A, a, g, n)
+
+def client_key(user, passphrase, s, B, u, keys):
+    
+    A, a, g, n = keys
+
+    # We don't trust the host.  Perhaps the host is being spoofed.
+
+    if B <= 0 or n <= B:
+        raise ImproperKeyValue
+
+    # Calculate the shared, secret session key.
+    
+
+    
+    x = private_key(user, s, passphrase)
+
+    v = pow(g, x, n)
+    t = B
+    if t < v:
+        t = t + n
+        
+    S = pow(t - v, a + u * x, n)
+    K = hash(S)
+    
+    # Compute the authentication proof.
+    # This verifies that we do indeed know the same session key,
+    # implying that we knew the correct password (even though the host
+    # doesn't know the password!)
+
+    m = client_authenticator(K, n, g, user, s, A, B, u)
+
+    return (K, m)
+
+# The next function is called from the host side.
+
+def lookup(user, A, passwd=None):
+
+    """Look the user up in the passwd database, calculate our version of
+    the session key, and return it along with a keyed hash of the values
+    used in the calculation as proof.  The client must match this proof.
+
+    - user: the username to be validated. 
+    - A: the authentication key used in client_authenticator
+    - passwd: A tuple of values, containing (secret, verifier, pfid).  If no
+      passwd is provided when calling this method, the internal password
+      file will be used (see read_passwd())."""
+
+    if passwd == None:
+        read_passwd()   # no way to specify the filename...
+        if not passwd.has_key(user):
+            raise NoSuchUser, user
+        s, v, pfid = passwd[user]
+    else:
+        s, v, pfid = passwd
+
+    pf = pflist[pfid]
+    n, g = pf
+
+    # We don't trust the client, who might be trying to send bogus data in
+    # order to break the protocol.
+
+    if A <= 0 or n <= A:
+            raise ImproperKeyValue
+
+    # Pick our random public keys.
+
+    while 1:
+        b = random_long(ablen)
+        B = (v + pow(g, b, n)) % n
+        if B != 0: break
+    u = pow(g, random_long(tlen), n)
+
+    # Calculate the (private, shared secret) session key.
+
+    t = (A * pow(v, u, n)) % n
+    if t <= 1 or t + 1 == n:
+        raise ImproperKeyValue  # WeakKeyValue -- could be our fault so retry
+    S = pow(t, b, n)
+    K = hash(S)
+
+    # Create the proof using a keyed hash.
+
+    m = client_authenticator(K, n, g, user, s, A, B, u)
+
+    return s, B, u, K, m
+
+# These two functions calculate the "proofs": keyed hashes of values used
+# in the computation of the key.
+
+def client_authenticator(K, n, g, user, s, A, B, u):
+    return hmac(K, hash(n) + hash(g) + hash(user) + s + `A` + `B` + `u`)
+
+def host_authenticator(K, A, m):
+    return hmac(K, `A` + m)
+
+# Simple password file management.
+
+def read_passwd(filename = 'passwd'):
+    global passwd
+    try:
+        f = open(filename)
+        passwd = pickle.load(f)
+        f.close()
+    except:
+        passwd = {}
+
+def write_passwd(filename = 'passwd'):
+    f = open(filename, 'w')
+    pickle.dump(passwd, f)
+    f.close()
+
+# To set a user's initial password, run "python SRP.py" and issue a
+# 'passwd user' command, followed by 'save'.  EOF to exit, 'quit' to abort.
+
+if __name__ == '__main__':
+    from cmd import Cmd
+    class srp(Cmd):
+        def __init__(self):
+            self.saved = 1
+        def emptyline(self):
+            pass
+        def do_EOF(self, arg):
+            print
+            if not self.saved:
+                        print 'passwd file not saved; "quit" to abort or "save" first.'
+                        return
+            return 1
+        def do_quit(self, arg):
+            return 1
+        def do_list(self, arg):
+            print passwd.keys()
+        def do_passwd(self, user):
+            new_passwd(user)
+            self.saved = 0
+        def do_del(self, user):
+            if passwd.has_key(user):
+                        del(passwd[user])
+            self.saved = 0
+        def do_save(self, arg):
+            write_passwd()
+            self.saved = 1
+    interp = srp()
+    interp.prompt = "SRP> "
+    read_passwd()
+    interp.cmdloop()
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/__init__.py ZODB3-Auth/ZEO/auth/__init__.py
--- ZODB3/ZEO/auth/__init__.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/__init__.py	Sat Jan 25 09:44:24 2003
@@ -0,0 +1,13 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/auth_plaintext.py ZODB3-Auth/ZEO/auth/auth_plaintext.py
--- ZODB3/ZEO/auth/auth_plaintext.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/auth_plaintext.py	Sat Jan 25 09:53:17 2003
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Implements plaintext password authentication. The password is stored
+in an SHA hash in the Database. The client sends over the plaintext
+password, and the SHA hashing is done on the server side. 
+ 
+This mechanism offers *no network security at all*; the only security is
+provided by not storing plaintext passwords on disk.  (See the auth_srp
+module for a secure mechanism)
+"""
+
+import sha
+from ZEO.auth.storage import AuthZEOStorage
+
+class StorageClass(AuthZEOStorage):
+    def auth(self, username, password):
+        try:
+            dbpw = self.database.get_password(username)
+        except LookupError:
+            return 0
+        password = sha.new(password).hexdigest()
+        return self.finish_auth(dbpw == password)
+    
+class Client:
+    def __init__(self, stub):
+        self.stub = stub
+
+    def start(self, username, password):
+        method = self.stub.extensionMethod('auth')
+        return method(username, password)
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/auth_sha.py ZODB3-Auth/ZEO/auth/auth_sha.py
--- ZODB3/ZEO/auth/auth_sha.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/auth_sha.py	Sat Jan 25 09:53:23 2003
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Implements simple hashed password authentication. The password is
+stored in an SHA hash in the Database. The client sends over the hashed
+password, which is verified by the server.
+ 
+This mechanism offers *very weak network security*; the password hash is
+capturable and the mechanism is vulnerable to trivial replay attacks.
+(See the auth_srp module for a secure mechanism)
+"""
+
+import sha
+
+from ZEO.auth.storage import AuthZEOStorage
+
+class StorageClass(AuthZEOStorage):
+    def auth(self, username, password):
+        try:
+            dbpw = self.database.get_password(username)
+        except LookupError:
+            return 0
+        return self.finish_auth(dbpw == password)
+
+class Client:
+    def __init__(self, stub):
+        self.stub = stub
+
+    def start(self, username, password):
+        method = self.stub.extensionMethod('auth')
+        return method(username, sha.new(password).hexdigest())
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/auth_srp.py ZODB3-Auth/ZEO/auth/auth_srp.py
--- ZODB3/ZEO/auth/auth_srp.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/auth_srp.py	Sat Jan 25 11:15:33 2003
@@ -0,0 +1,147 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Implements authentication based on the SRP protocol
+(http://srp.stanford.edu/). The implementation is based on Tom Holyrod's
+SRPSocket server, which has been ported to the ZEO RPC. Parts of the
+SRPSocket (SRP.py and hmac.py) package have been allowed to be included
+in the ZEO distribution under the ZPL license and are included.
+ 
+SRPSocket is available at
+http://members.tripod.com/professor_tom/archives/srpsocket.html
+"""
+
+import base64, binascii
+import os
+from types import StringType
+
+from ZEO.ServerStub import StorageServer
+from ZEO.auth import SRP
+from ZEO.auth.database import Database
+from ZEO.auth.storage import AuthZEOStorage
+
+class SRPDatabase(Database):
+    """The SRP protocol requires that the password database store a pair of
+       values per username: salt (s) and verifier (v). The SRPDatabase
+       class implements this mechanism as an extension of the simple
+       Datbase class.
+    """
+    def load(self):
+        filename = self.filename
+        if not filename or not os.path.exists(filename):
+            return
+        
+        fd = open(filename)
+        for line in fd.readlines():
+            # SRP requires salt and verifier, so we get back 3 fields
+            try:
+                username, salt, verifier = line[:-1].split(':')
+                salt = base64.decodestring(salt)
+            except (ValueError, binascii.Error):
+                raise ValueError, "Password database not in SRP format."
+            self._users[username] = salt, long(verifier)
+            
+    def _store_password(self, username, password):
+        if type(password) is not StringType:
+            raise TypeError, "password must be a string"
+
+        # XXX: SRPSocket always uses zero as pfid. Is this correct?
+        pf = SRP.pflist[0]
+        salt, verifier = SRP.create_new_verifier(username, password, pf)
+        self._users[username] = salt, verifier
+        
+    def save(self, fd=None):
+        filename = self.filename
+
+        if not fd:
+            fd = open(filename, 'w')
+        
+        for username, (salt, verifier) in self._users.items():
+            # The salt is encoded into valid text, and decoded on load()
+            salt = base64.encodestring(salt)[:-1]
+            fd.write('%s:%s:%s\n' % (username, salt, verifier))
+
+class StorageClass(AuthZEOStorage):
+    """SRP is a protocol that happens in two steps: identification and
+       validation. The client contacts the server with the username and
+       gets a salt back, calls SRP.client_key() with the results and
+       then proceeds to call the server to do the final autentication
+       step. This class implements the server part of the protocol.
+    """
+    def srp_identify(self, username, A):
+        try:
+           retval = self.database.get_password(username)
+        except LookupError:
+           return 0
+        if retval is None:
+            return 0
+
+        s, v = retval
+        
+        # salt, verifier and pfid, which is zero in SRPSocket
+        # XXX: Correct? See above for same issue.
+        retval = SRP.lookup(username, A, (s, v, 0))
+        s, B, u, self.K, self.m = retval
+        self.A = A
+        
+        return s, B, u
+    
+    def srp_authenticate(self, client_m):
+        if self.m != client_m:
+            return 0
+       
+        # At this point we trust the client is who he says he is. 
+        # Of course, the client still needs to validate the 
+        # server, but it's up to him to complain.
+        self.finish_auth(1)
+        return SRP.host_authenticator(self.K, self.A, self.m)
+        
+class Client:
+    """Client-side class of SRP authentication, uses the two RPC stubs to
+       call the methods declared in the server class.
+    """
+    def __init__(self, stub):
+        self.stub = stub
+
+    def call_identify(self, username, A):
+        method = self.stub.extensionMethod('srp_identify')
+        return method(username, A)
+
+    def call_authenticate(self, m):
+        method = self.stub.extensionMethod('srp_authenticate')
+        return method(m)
+    
+    def start(self, username, password):
+        keys = SRP.client_begin(username)
+        A = keys[0]
+
+        retval = self.call_identify(username, A)
+        if not retval:
+            return 0
+        s, B, u = retval
+
+        K, client_m = SRP.client_key(username, password, s, B, u, keys)
+
+        server_m = self.call_authenticate(client_m)
+        if not server_m:
+            return 0
+
+        # At this point the server has authenticated us, and all that's
+        # left is authenticating the server ourselves.
+        if server_m != SRP.host_authenticator(K, A, client_m):
+            return 0
+       
+        return 1
+
+# StorageServer checks here for DatabaseClass
+DatabaseClass = SRPDatabase
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/database.py ZODB3-Auth/ZEO/auth/database.py
--- ZODB3/ZEO/auth/database.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/database.py	Sat Jan 25 11:15:25 2003
@@ -0,0 +1,96 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import os
+import sha
+from types import StringType
+
+class Database:
+    """Abstracts a password database. This class is used both in the
+       authentication process (via get_password()) and by client scripts
+       that manage the password database file.
+
+       The password file is a simple, colon-separated text file mapping
+       usernames to password hashes. The hashes are SHA hex digests
+       produced from the password string.
+    """
+    def __init__(self, filename):
+        """Creates a new Database instance.
+
+           filename -- a string containing the full pathname of the
+               password database file. Must be readable by the user
+               running ZEO. Must be writeable by any client script that
+               accesses the database.
+        """
+        
+        self._users = {}
+        self.filename = filename
+        self.load()
+        
+    def save(self, fd=None):
+        filename = self.filename
+
+        if not fd:
+            fd = open(filename, 'w')
+        
+        for username, hash in self._users.items():
+            fd.write('%s:%s\n' % (username, hash))
+            
+    def load(self):
+        filename = self.filename
+        if not filename or not os.path.exists(filename):
+            return
+        
+        fd = open(filename)
+        for line in fd.readlines():
+            try:
+                username, hash = line[:-1].split(':')
+            except ValueError:
+                raise ValueError, "Password database in invalid format"
+            self._users[username] = hash
+
+    def _store_password(self, username, password):
+        if type(password) is not StringType:
+            raise TypeError, "password must be a string"
+        self._users[username] = self.hash(password)
+
+    def get_password(self, username):
+        """Returns password hash for specified username.
+
+           Callers must check for LookupError, which is raised in the
+           case of a non-existent user specified.
+        """
+        if not self._users.has_key(username):
+            raise LookupError, "No such user: %s" % username
+        return self._users[username]
+    
+    def hash(self, s):
+        return sha.new(s).hexdigest()
+
+    def add_user(self, username, password):
+        if self._users.has_key(username):
+            raise LookupError, "User %s already exists" % username
+        if type(username) <> StringType:
+            raise TypeError, "username must be a string"
+        self._store_password(username, password)
+
+    def del_user(self, username):
+        if not self._users.has_key(username):
+            raise LookupError, "No such user: %s" % username
+        del self._users[username]
+
+    def change_password(self, username, password):
+        if not self._users.has_key(username):
+            raise LookupError, "No such user: %s" % username
+        self._store_password(username, password)
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/hmac.py ZODB3-Auth/ZEO/auth/hmac.py
--- ZODB3/ZEO/auth/hmac.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/hmac.py	Sat Jan 25 10:06:05 2003
@@ -0,0 +1,43 @@
+"""HMAC -- keyed hashing for message authentication, as described in rfc2104.
+
+Author: Tom Holroyd <tomh@kurage.nimh.nih.gov>
+Part of the SRPSocket package: 
+http://members.tripod.com/professor_tom/archives/srpsocket.html
+"""
+
+import sha
+
+BLEN = 64
+ipad = map(ord, "\x36" * BLEN)
+opad = map(ord, "\x5C" * BLEN)
+
+def hash(s):
+    return sha.new(s).digest()
+
+def hmac(key, text):
+    """Given strings 'key' and 'text', produce an HMAC digest."""
+
+    # If the key is longer than BLEN, hash it first.  The result must
+    # be less than BLEN bytes.  This depends on the hash function used;
+    # sha1 and md5 are both OK.
+
+    l = len(key)
+    if l > BLEN:
+        key = hash(key)
+        l = len(key)
+
+    # Pad the key with zeros to BLEN bytes.
+
+    key = key + '\0' * (BLEN - l)
+    key = map(ord, key)
+
+    # Now compute the HMAC.
+
+    l = map(lambda x, y: x ^ y, key, ipad)
+    s = reduce(lambda x, y: x + chr(y), l, '')
+    s = hash(s + text)
+    l = map(lambda x, y: x ^ y, key, opad)
+    t = reduce(lambda x, y: x + chr(y), l, '')
+    s = hash(t + s)
+
+    return s
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/storage.py ZODB3-Auth/ZEO/auth/storage.py
--- ZODB3/ZEO/auth/storage.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/storage.py	Sat Jan 25 09:44:24 2003
@@ -0,0 +1,34 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+from ZEO.Exceptions import AuthError
+from ZEO.StorageServer import ZEOStorage
+
+class AuthZEOStorage(ZEOStorage):
+    def __init_(self, *args, **kwargs):
+        ZEOStorage.__init__(self, *args, **kwargs)
+        self.authenticated = 0
+
+    def set_database(self, database):
+        self.database = database
+        
+    def finish_auth(self, authenticated):
+        self.authenticated = authenticated
+        return authenticated
+    
+    def register(self, storage_id, read_only):
+        if not self.authenticated:
+            raise AuthError, "Client was never authenticated with server!"
+        
+        ZEOStorage.register(self, storage_id, read_only)
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/auth/zpasswd.py ZODB3-Auth/ZEO/auth/zpasswd.py
--- ZODB3/ZEO/auth/zpasswd.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/auth/zpasswd.py	Sat Jan 25 11:32:00 2003
@@ -0,0 +1,107 @@
+#!python
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Usage:
+        zpasswd [-cd] passwordfile username
+        zpasswd -b[cd] passwordfile username password
+
+        zpasswd -n[d] username
+        zpasswd -nb[d] username password
+ -c  Create a new file.
+ -d  Delete user
+ -n  Don't update file; display results on stdout.
+ -b  Use the password from the command line rather than prompting for it."""
+
+import sys
+import getopt
+import getpass
+
+from ZEO.auth.database import Database
+from ZEO.auth.auth_srp import SRPDatabase
+
+try:
+    opts, args = getopt.getopt(sys.argv[1:], 'cdnbs')
+except getopt.GetoptError:
+    # print help information and exit:
+    print __doc__
+    sys.exit(2)
+
+stdout = 0
+create = 0
+delete = 0
+prompt = 1
+srp = 0
+
+for opt, arg in opts:
+    if opt in ("-h", "--help"):
+        print __doc__
+        sys.exit()
+    if opt == "-n":
+        stdout = 1
+    if opt == "-c":
+        create = 1
+    if opt == "-d":
+        delete = 1
+    if opt == "b":
+        prompt = 0
+    if opt == "-s":
+        srp = 1
+
+if create and delete:
+    print "Can't create and delete at the same time"
+    sys.exit(3)
+
+if len(args) < 2:
+    print __doc__
+    sys.exit()
+
+output = args[0]
+username = args[1]
+
+if not delete:
+    if len(args) > 3:
+        print __doc__
+        sys.exit()
+        
+    if prompt:
+        password = getpass.getpass('Enter passphrase: ')
+    else:
+        password = args[2]
+
+if srp:
+    db = SRPDatabase(output)
+else:
+    db = Database(output)
+
+if create:
+    try:
+        db.add_user(username, password)
+    except LookupError:
+        print 'The username already exists'
+        sys.exit(4)
+    if stdout:
+        db.save(fd=sys.stdout)
+    else:
+        db.save()
+    
+if delete:
+    try:
+        db.del_user(username)
+    except LockupError:
+        print 'The username doesn\'t exist'
+        sys.exit(5)
+    if stdout:
+        db.save(fd=sys.stdout)
+    else:
+        db.save()
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/runzeo.py ZODB3-Auth/ZEO/runzeo.py
--- ZODB3/ZEO/runzeo.py	Fri Jan 24 20:37:20 2003
+++ ZODB3-Auth/ZEO/runzeo.py	Sat Jan 25 10:19:17 2003
@@ -1,7 +1,7 @@
 #!python
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -89,7 +89,10 @@
                  "t:", "timeout=", float)
         self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=",
                  self.handle_monitor_address)
-
+        self.add('auth_protocol', 'zeo.auth_protocol', None,
+                 'auth-protocol=', default=None)
+        self.add('auth_filename', 'zeo.auth_filename', None, 
+                 'auth-filename=')
 
 class ZEOOptions(ZDOptions, ZEOOptionsMixin):
 
@@ -189,7 +192,9 @@
             read_only=self.options.read_only,
             invalidation_queue_size=self.options.invalidation_queue_size,
             transaction_timeout=self.options.transaction_timeout,
-            monitor_address=self.options.monitor_address)
+            monitor_address=self.options.monitor_address,
+            auth_protocol=self.options.auth_protocol,
+            auth_filename=self.options.auth_filename)
 
     def loop_forever(self):
         import ThreadedAsync.LoopCallback
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/schema.xml ZODB3-Auth/ZEO/schema.xml
--- ZODB3/ZEO/schema.xml	Wed Jan 22 18:38:11 2003
+++ ZODB3-Auth/ZEO/schema.xml	Sat Jan 25 10:20:52 2003
@@ -29,4 +29,25 @@
 
   <section name="*" type="eventlog" attribute="eventlog" required="no" />
 
+  <key name="auth-protocol" datatype="string" required="yes">
+    <description>
+      Can be one of `none', `plaintext', `sha' or `srp'.
+
+      none is the default value: when used, no authentication is done.
+
+      plaintext sends the username and password as plaintext.
+
+      sha sends sha hashes over the wire.
+
+      srp is a more secure alternative, see http://srp.stanford.edu/ for
+      more information about the protocol.
+    </description>
+  </key>
+  
+  <key name="auth-filename" datatype="string" required="no">
+    <description>
+      Filename pointing to a password database used for authenication.
+    </description>
+  </key>
+
 </schema>
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/ZEO/tests/testAuth.py ZODB3-Auth/ZEO/tests/testAuth.py
--- ZODB3/ZEO/tests/testAuth.py	Wed Dec 31 21:00:00 1969
+++ ZODB3-Auth/ZEO/tests/testAuth.py	Sat Jan 25 13:08:20 2003
@@ -0,0 +1,125 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Test suite for AuthZEO."""
+
+import glob
+import os
+import time
+import unittest
+
+from ThreadedAsync import LoopCallback
+from ZEO.auth.database import Database
+from ZEO.auth.auth_srp import SRPDatabase
+from ZEO.ClientStorage import ClientStorage
+from ZEO.StorageServer import StorageServer
+from ZODB.FileStorage import FileStorage
+
+storage = FileStorage('auth-test.fs')
+
+SOCKET='auth-test-socket'
+STORAGES={'1': storage}
+
+class BaseTest(unittest.TestCase):
+    def createDB(self, name):
+        if os.path.exists(name):
+            os.remove(self.database)        
+        if name.endswith('srp'):
+            db = SRPDatabase(name)
+        else:
+            db = Database(name)
+
+        db.add_user('foo', 'bar')
+        db.save()
+        
+    def setUp(self):
+        self.createDB(self.database)
+        self.pid =  os.fork()
+        if not self.pid:
+            self.server = StorageServer(SOCKET, STORAGES,
+                                        auth_protocol=self.protocol,
+                                        auth_filename=self.database)
+            LoopCallback.loop()
+
+    def tearDown(self):
+        os.kill(self.pid, 9)
+        os.remove(self.database)
+        os.remove(SOCKET)
+
+        for file in glob.glob('auth-test.fs*'):
+            os.remove(file)
+            
+    def checkOK(self):
+        # Sleep for 0.2 seconds to give the server some time to start up
+        # seems to be needed before and after creating the storage
+        time.sleep(self.wait)
+        cs = ClientStorage(SOCKET, wait=0, username='foo', password='bar')
+        time.sleep(self.wait)
+
+        if cs is None:
+            raise AssertionError, \
+                  "authentication for %s failed" % self.protocol
+        
+        if cs._connection == None:
+            raise AssertionError, \
+                  "authentication for %s failed" % self.protocol
+        
+        cs._connection.poll()
+        if not cs.is_connected():
+             raise AssertionError, \
+                  "authentication for %s failed" % self.protocol
+    
+    def checkNOK(self):
+        time.sleep(self.wait)
+        cs = ClientStorage(SOCKET, wait=0, username='foo', password='noogie')
+        time.sleep(self.wait)
+       
+        # Normally a wrong password will return None immediately. 
+        if cs is None:
+            return
+
+        if cs._connection is None:
+            return
+
+        cs._connection.poll()
+    
+        if cs.is_connected():
+             raise AssertionError, "authenticated with incorrect password"
+            
+class PlainTextAuth(BaseTest):
+    protocol = 'plaintext'
+    database = 'authdb.sha'
+    wait = 0.2
+    
+class SHAAuth(BaseTest):
+    protocol = 'sha'
+    database = 'authdb.sha'
+    wait = 0.5
+    
+class SRPAuth(BaseTest):
+    protocol = 'srp'
+    database = 'authdb.srp'
+    wait = 1.0
+    
+test_classes = [PlainTextAuth, SHAAuth, SRPAuth]
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for klass in test_classes:
+        sub = unittest.makeSuite(klass, 'check')
+        suite.addTest(sub)
+    return suite
+
+if __name__ == "__main__":
+    unittest.main(defaultTest='test_suite')
+
diff -urN --exclude=build --exclude=dist --exclude=CVS ZODB3/setup.py ZODB3-Auth/setup.py
--- ZODB3/setup.py	Wed Jan 22 18:14:28 2003
+++ ZODB3-Auth/setup.py	Sat Jan 25 11:31:14 2003
@@ -143,7 +143,7 @@
 
 packages = ["BDBStorage", "BDBStorage.tests",
             "BTrees", "BTrees.tests",
-            "ZEO", "ZEO.zrpc", "ZEO.tests",
+            "ZEO", "ZEO.zrpc", "ZEO.auth", "ZEO.tests", 
             "ZODB", "ZODB.tests",
             "Persistence",
             "ThreadedAsync",
@@ -165,6 +165,7 @@
            "Tools/zeopack.py",
            "ZConfig/scripts/zconfig",
            "ZEO/runzeo.py",
+           "ZEO/auth/zpasswd.py",
            "ZEO/mkzeoinst.py",
            "zdaemon/zdrun.py",
            "zdaemon/zdctl.py",

--3MwIy2ne0vdjdPXF--