[Zope-Checkins] CVS: ZODB3/ZEO/auth - hmac.py:1.2 base.py:1.2 auth_digest.py:1.2 __init__.py:1.2 .cvsignore:1.2
Jeremy Hylton
jeremy@zope.com
Fri, 30 May 2003 15:21:27 -0400
Update of /cvs-repository/ZODB3/ZEO/auth
In directory cvs.zope.org:/tmp/cvs-serv25334/ZEO/auth
Added Files:
hmac.py base.py auth_digest.py __init__.py .cvsignore
Log Message:
Merge ZODB3-auth-branch and bump a few version numbers.
After the merge, I made several Python 2.1 compatibility changes for
the auth code.
=== ZODB3/ZEO/auth/hmac.py 1.1 => 1.2 ===
--- /dev/null Fri May 30 15:21:27 2003
+++ ZODB3/ZEO/auth/hmac.py Fri May 30 15:20:57 2003
@@ -0,0 +1,97 @@
+"""HMAC (Keyed-Hashing for Message Authentication) Python module.
+
+Implements the HMAC algorithm as described by RFC 2104.
+"""
+
+def _strxor(s1, s2):
+ """Utility method. XOR the two strings s1 and s2 (must have same length).
+ """
+ return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))
+
+# The size of the digests returned by HMAC depends on the underlying
+# hashing module used.
+digest_size = None
+
+class HMAC:
+ """RFC2104 HMAC class.
+
+ This supports the API for Cryptographic Hash Functions (PEP 247).
+ """
+
+ def __init__(self, key, msg = None, digestmod = None):
+ """Create a new HMAC object.
+
+ key: key for the keyed hash object.
+ msg: Initial input for the hash, if provided.
+ digestmod: A module supporting PEP 247. Defaults to the md5 module.
+ """
+ if digestmod is None:
+ import md5
+ digestmod = md5
+
+ self.digestmod = digestmod
+ self.outer = digestmod.new()
+ self.inner = digestmod.new()
+ self.digest_size = digestmod.digest_size
+
+ blocksize = 64
+ ipad = "\x36" * blocksize
+ opad = "\x5C" * blocksize
+
+ if len(key) > blocksize:
+ key = digestmod.new(key).digest()
+
+ key = key + chr(0) * (blocksize - len(key))
+ self.outer.update(_strxor(key, opad))
+ self.inner.update(_strxor(key, ipad))
+ if msg is not None:
+ self.update(msg)
+
+## def clear(self):
+## raise NotImplementedError, "clear() method not available in HMAC."
+
+ def update(self, msg):
+ """Update this hashing object with the string msg.
+ """
+ self.inner.update(msg)
+
+ def copy(self):
+ """Return a separate copy of this hashing object.
+
+ An update to this copy won't affect the original object.
+ """
+ other = HMAC("")
+ other.digestmod = self.digestmod
+ other.inner = self.inner.copy()
+ other.outer = self.outer.copy()
+ return other
+
+ def digest(self):
+ """Return the hash value of this hashing object.
+
+ This returns a string containing 8-bit data. The object is
+ not altered in any way by this function; you can continue
+ updating the object after calling this function.
+ """
+ h = self.outer.copy()
+ h.update(self.inner.digest())
+ return h.digest()
+
+ def hexdigest(self):
+ """Like digest(), but returns a string of hexadecimal digits instead.
+ """
+ return "".join([hex(ord(x))[2:].zfill(2)
+ for x in tuple(self.digest())])
+
+def new(key, msg = None, digestmod = None):
+ """Create a new hashing object and return it.
+
+ key: The starting key for the hash.
+ msg: if available, will immediately be hashed into the object's starting
+ state.
+
+ You can now feed arbitrary strings into the object using its update()
+ method, and can ask for the hash value at any time by calling its digest()
+ method.
+ """
+ return HMAC(key, msg, digestmod)
=== ZODB3/ZEO/auth/base.py 1.1 => 1.2 ===
--- /dev/null Fri May 30 15:21:27 2003
+++ ZODB3/ZEO/auth/base.py Fri May 30 15:20:57 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)
=== ZODB3/ZEO/auth/auth_digest.py 1.1 => 1.2 ===
--- /dev/null Fri May 30 15:21:27 2003
+++ ZODB3/ZEO/auth/auth_digest.py Fri May 30 15:20:57 2003
@@ -0,0 +1,142 @@
+##############################################################################
+#
+# 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 sha
+import struct
+import time
+
+from ZEO.auth.base import Database, Client
+from ZEO.StorageServer import ZEOStorage
+from ZEO.Exceptions 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
=== ZODB3/ZEO/auth/__init__.py 1.1 => 1.2 ===
--- /dev/null Fri May 30 15:21:27 2003
+++ ZODB3/ZEO/auth/__init__.py Fri May 30 15:20:57 2003
@@ -0,0 +1,31 @@
+##############################################################################
+#
+# 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 == 'sha':
+ from auth_sha import StorageClass, SHAClient, Database
+ return StorageClass, SHAClient, Database
+ elif 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
+
=== ZODB3/ZEO/auth/.cvsignore 1.1 => 1.2 ===
--- /dev/null Fri May 30 15:21:27 2003
+++ ZODB3/ZEO/auth/.cvsignore Fri May 30 15:20:57 2003
@@ -0,0 +1,2 @@
+*.pyo
+*.pyc