[Zope3-checkins] CVS: ZODB4/src/zodb/zeo/auth - base.py:1.2 auth_digest.py:1.2 __init__.py:1.2

Jeremy Hylton jeremy@zope.com
Thu, 19 Jun 2003 17:41:39 -0400


Update of /cvs-repository/ZODB4/src/zodb/zeo/auth
In directory cvs.zope.org:/tmp/cvs-serv15960/src/zodb/zeo/auth

Added Files:
	base.py auth_digest.py __init__.py 
Log Message:
Merge ZODB3-2-merge branch to the head.

This completes the porting of bug fixes and random improvements from
ZODB 3.2 to ZODB 4.


=== ZODB4/src/zodb/zeo/auth/base.py 1.1 => 1.2 ===
--- /dev/null	Thu Jun 19 17:41:38 2003
+++ ZODB4/src/zodb/zeo/auth/base.py	Thu Jun 19 17:41:08 2003
@@ -0,0 +1,122 @@
+##############################################################################
+#
+# 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
+#
+##############################################################################
+"""Base classes for defining an authentication protocol.
+
+Database -- abstract base class for password database
+Client -- abstract base class for authentication client
+"""
+
+import os
+import sha
+
+class Client:
+    # Subclass should override to list the names of methods that
+    # will be called on the server.
+    extensions = []
+
+    def __init__(self, stub):
+        self.stub = stub
+        for m in self.extensions:
+            setattr(self.stub, m, self.stub.extensionMethod(m))
+
+def sort(L):
+    """Sort a list in-place and return it."""
+    L.sort()
+    return L
+    
+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, realm=None):
+        """Creates a new Database
+
+        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.
+
+        realm: the realm name (a string)
+        """
+        self._users = {}
+        self.filename = filename
+        self.realm = realm
+        self.load()
+        
+    def save(self, fd=None):
+        filename = self.filename
+
+        if not fd:
+            fd = open(filename, 'w')
+        if self.realm:
+            print >> fd, "realm", self.realm
+
+        for username in sort(self._users.keys()):
+            print >> fd, "%s: %s" % (username, self._users[username])
+            
+    def load(self):
+        filename = self.filename
+        if not filename:
+            return
+
+        if not os.path.exists(filename):
+            return
+        
+        fd = open(filename)
+        L = fd.readlines()
+        if L[0].startswith("realm "):
+            line = L.pop(0).strip()
+            self.realm = line[len("realm "):]
+            
+        for line in L:
+            username, hash = line.strip().split(":", 1)
+            self._users[username] = hash.strip()
+
+    def _store_password(self, username, password):
+        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 does already exist" % username
+        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)


=== ZODB4/src/zodb/zeo/auth/auth_digest.py 1.1 => 1.2 ===
--- /dev/null	Thu Jun 19 17:41:38 2003
+++ ZODB4/src/zodb/zeo/auth/auth_digest.py	Thu Jun 19 17:41:08 2003
@@ -0,0 +1,143 @@
+##############################################################################
+#
+# 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
+#
+##############################################################################
+"""Digest authentication for ZEO
+
+This authentication mechanism follows the design of HTTP digest
+authentication (RFC 2069).  It is a simple challenge-response protocol
+that does not send passwords in the clear, but does not offer strong
+security.  The RFC discusses many of the limitations of this kind of
+protocol.
+
+Guard the password database as if it contained plaintext passwords.
+It stores the hash of a username and password.  This does not expose
+the plaintext password, but it is sensitive nonetheless.  An attacker
+with the hash can impersonate the real user.  This is a limitation of
+the simple digest scheme.
+
+HTTP is a stateless protocol, and ZEO is a stateful protocol.  The
+security requirements are quite different as a result.  The HTTP
+protocol uses a nonce as a challenge.  The ZEO protocol requires a
+separate session key that is used for message authentication.  We
+generate a second nonce for this purpose; the hash of nonce and
+user/realm/password is used as the session key.  XXX I'm not sure if
+this is a sound approach; SRP would be preferred.
+"""
+
+import base64
+import os
+import random
+import sha
+import struct
+import time
+
+from zodb.zeo.auth.base import Database, Client
+from zodb.zeo.server import ZEOStorage
+from zodb.zeo.interfaces import AuthError
+
+def get_random_bytes(n=8):
+    if os.path.exists("/dev/urandom"):
+        f = open("/dev/urandom")
+        s = f.read(n)
+        f.close()
+    else:
+        L = [chr(random.randint(0, 255)) for i in range(n)]
+        s = "".join(L)
+    return s
+
+def hexdigest(s):
+    return sha.new(s).hexdigest()
+
+class DigestDatabase(Database):
+    def __init__(self, filename, realm=None):
+        Database.__init__(self, filename, realm)
+        
+        # Initialize a key used to build the nonce for a challenge.
+        # We need one key for the lifetime of the server, so it
+        # is convenient to store in on the database.
+        self.noncekey = get_random_bytes(8)
+
+    def _store_password(self, username, password):
+        dig = hexdigest("%s:%s:%s" % (username, self.realm, password))
+        self._users[username] = dig
+
+def session_key(h_up, nonce):
+    # The hash itself is a bit too short to be a session key.
+    # HMAC wants a 64-byte key.  We don't want to use h_up
+    # directly because it would never change over time.  Instead
+    # use the hash plus part of h_up.
+    return sha.new("%s:%s" % (h_up, nonce)).digest() + h_up[:44]
+
+class StorageClass(ZEOStorage):
+    def set_database(self, database):
+        assert isinstance(database, DigestDatabase)
+        self.database = database
+        self.noncekey = database.noncekey
+
+    def _get_time(self):
+        # Return a string representing the current time.
+        t = int(time.time())
+        return struct.pack("i", t)
+
+    def _get_nonce(self):
+        # RFC 2069 recommends a nonce of the form
+        # H(client-IP ":" time-stamp ":" private-key)
+        dig = sha.sha()
+        dig.update(str(self.connection.addr))
+        dig.update(self._get_time())
+        dig.update(self.noncekey)
+        return dig.hexdigest()
+
+    def auth_get_challenge(self):
+        """Return realm, challenge, and nonce."""
+        self._challenge = self._get_nonce()
+        self._key_nonce = self._get_nonce()
+        return self.auth_realm, self._challenge, self._key_nonce
+
+    def auth_response(self, resp):
+        # verify client response
+        user, challenge, response = resp
+
+        # Since zrpc is a stateful protocol, we just store the nonce
+        # we sent to the client.  It will need to generate a new
+        # nonce for a new connection anyway.
+        if self._challenge != challenge:
+            raise ValueError, "invalid challenge"
+
+        # lookup user in database
+        h_up = self.database.get_password(user)
+
+        # regeneration resp from user, password, and nonce
+        check = hexdigest("%s:%s" % (h_up, challenge))
+        if check == response:
+            self.connection.setSessionKey(session_key(h_up, self._key_nonce))
+        return self.finish_auth(check == response)
+
+    extensions = [auth_get_challenge, auth_response]
+
+class DigestClient(Client):
+    extensions = ["auth_get_challenge", "auth_response"]
+
+    def start(self, username, realm, password):
+        _realm, challenge, nonce = self.stub.auth_get_challenge()
+        if _realm != realm:
+            raise AuthError("expected realm %r, got realm %r"
+                            % (_realm, realm))
+        h_up = hexdigest("%s:%s:%s" % (username, realm, password))
+        
+        resp_dig = hexdigest("%s:%s" % (h_up, challenge))
+        result = self.stub.auth_response((username, challenge, resp_dig))
+        if result:
+            return session_key(h_up, nonce)
+        else:
+            return None


=== ZODB4/src/zodb/zeo/auth/__init__.py 1.1 => 1.2 ===
--- /dev/null	Thu Jun 19 17:41:38 2003
+++ ZODB4/src/zodb/zeo/auth/__init__.py	Thu Jun 19 17:41:08 2003
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# 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
+#
+##############################################################################
+
+_auth_modules = {}
+
+def get_module(name):
+    if name == 'digest':
+        from auth_digest import StorageClass, DigestClient, DigestDatabase
+        return StorageClass, DigestClient, DigestDatabase
+    else:
+        return _auth_modules.get(name)
+
+def register_module(name, storage_class, client, db):
+    if _auth_modules.has_key(name):
+        raise TypeError, "%s is already registred" % name
+    _auth_modules[name] = storage_class, client, db
+