-
-
Save posativ/abfc30a6ef3097d159ed to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
commit 2163d70532c3c2e584d7a64e995be1e655790b1a | |
Author: Martin Zimmermann <info@posativ.org> | |
Date: Thu Aug 8 22:35:19 2013 +0200 | |
pbkdf2 draft, including migration | |
* PBKDF2 from Django project (crypto.py) | |
* migrates database on first request | |
* automatic round detection (useful for slow routers) | |
diff --git a/weave/__init__.py b/weave/__init__.py | |
index 4627cb9..2bbfd94 100755 | |
--- a/weave/__init__.py | |
+++ b/weave/__init__.py | |
@@ -30,7 +30,7 @@ import errno | |
import hashlib | |
import sqlite3 | |
-from os.path import join | |
+from os.path import join, isfile | |
from optparse import OptionParser, make_option, SUPPRESS_HELP | |
from urlparse import urlsplit | |
@@ -39,7 +39,7 @@ from werkzeug.serving import run_simple | |
from werkzeug.wrappers import Request, Response | |
from werkzeug.exceptions import HTTPException, NotFound, NotImplemented, InternalServerError | |
-from weave.minimal import user, storage, misc | |
+from weave.minimal import user, storage, misc, crypto | |
from weave.minimal.utils import encode | |
try: | |
@@ -144,9 +144,15 @@ class ReverseProxied(object): | |
class Weave(object): | |
+ # XXX: control via /etc/conf.d/weave-minimal or similar. | |
salt = r'\x14Q\xd4JbDk\x1bN\x84J\xd0\x05\x8a\x1b\x8b\xa6&V\x1b\xc5\x91\x97\xc4' | |
- def __init__(self, data_dir, registration): | |
+ def __init__(self, data_dir, registration, pbkdf2): | |
+ | |
+ if pbkdf2 == -1: | |
+ self.iterations = crypto.timeit() | |
+ else: | |
+ self.iterations = pbkdf2 | |
try: | |
os.makedirs(data_dir) | |
@@ -157,15 +163,41 @@ class Weave(object): | |
self.data_dir = data_dir | |
self.registration = registration | |
- def crypt(self, password): | |
- return hashlib.sha1(self.salt+password).hexdigest()[:16] | |
+ def crypt(self, password, iterations=None, oldstyle=False): | |
+ if oldstyle: | |
+ return hashlib.sha1(self.salt+password).hexdigest()[:16] | |
+ | |
+ return crypto.pbkdf2(password, self.salt, iterations or self.iterations) | |
+ | |
+ def auth(self, user, password, **kw): | |
+ | |
+ for db in os.listdir(self.data_dir): | |
+ if not isfile(db): | |
+ continue | |
+ | |
+ split = db.split('.') | |
+ if len(split) == 3 and split[0] == user: | |
+ return self.crypt(password, int(split[1])) == split[2] | |
+ elif len(split) == 2 and split[0] == user: | |
+ return self.crypt(password, oldstyle=True) == split[1] | |
+ | |
+ return False | |
+ | |
+ def dbpath(self, user): | |
- def dbpath(self, user, password): | |
- return join(self.data_dir, (user + '.' + self.crypt(password))) | |
+ for db in os.listdir(self.data_dir): | |
+ if not isfile(join(self.data_dir, db)): | |
+ continue | |
+ | |
+ if db.split('.')[0] == user: | |
+ return join(self.data_dir, db) | |
+ | |
+ assert False | |
def initialize(self, uid, password): | |
- dbpath = self.dbpath(uid, password) | |
+ dbpath = join(self.data_dir, | |
+ '%s.%i.%s' % (uid, self.iterations, self.crypt(password))) | |
try: | |
os.unlink(dbpath) | |
@@ -206,8 +238,8 @@ class Weave(object): | |
return self.wsgi_app(environ, start_response) | |
-def make_app(data_dir='.data/', prefix=None, base_url=None, register=False): | |
- application = Weave(data_dir, register) | |
+def make_app(data_dir='.data/', prefix=None, base_url=None, register=False, test=False): | |
+ application = Weave(data_dir, register, test) | |
application.wsgi_app = ReverseProxied(application.wsgi_app, prefix, base_url) | |
return application | |
@@ -224,15 +256,19 @@ def main(): | |
make_option("--register", dest="creds", default=None, | |
help="user:passwd credentials"), | |
make_option("--enable-registration", dest="registration", action="store_true", | |
- help="enable registration"), | |
+ help="enable user registration"), | |
make_option("--prefix", dest="prefix", default="/", | |
help="prefix support for broken servers, deprecated."), | |
make_option("--base-url", dest="base_url", default=None, | |
help="set your actual URI such as https://example.org/weave/"), | |
- make_option("--use-reloader", action="store_true", dest="reloader", | |
- help=SUPPRESS_HELP, default=False), | |
make_option("--version", action="store_true", dest="version", | |
+ help="show program's version number and exit", default=False), | |
+ | |
+ # devel | |
+ make_option("--use-reloader", action="store_true", dest="reloader", | |
help=SUPPRESS_HELP, default=False), | |
+ make_option("--pbkdf2-iterations", dest="pbkdf2", default=-1, type=int, | |
+ help=SUPPRESS_HELP) | |
] | |
parser = OptionParser(option_list=options) | |
@@ -244,7 +280,7 @@ def main(): | |
sys.exit(0) | |
prefix = options.prefix.rstrip('/') | |
- app = make_app(options.data_dir, prefix, options.base_url, options.registration) | |
+ app = make_app(options.data_dir, prefix, options.base_url, options.registration, options.pbkdf2) | |
if options.creds: | |
diff --git a/weave/minimal/crypto.py b/weave/minimal/crypto.py | |
new file mode 100644 | |
index 0000000..bcf4002 | |
--- /dev/null | |
+++ b/weave/minimal/crypto.py | |
@@ -0,0 +1,101 @@ | |
+# -*- encoding: utf-8 -*- | |
+# | |
+# Copyright (c) Django Software Foundation and individual contributors. | |
+# All rights reserved. | |
+ | |
+import hmac | |
+import time | |
+import struct | |
+import base64 | |
+import hashlib | |
+import binascii | |
+import operator | |
+ | |
+ | |
+def _bin_to_long(x): | |
+ """ | |
+ Convert a binary string into a long integer | |
+ | |
+ This is a clever optimization for fast xor vector math | |
+ """ | |
+ return int(binascii.hexlify(x), 16) | |
+ | |
+ | |
+def _long_to_bin(x, hex_format_string): | |
+ """ | |
+ Convert a long integer into a binary string. | |
+ hex_format_string is like "%020x" for padding 10 characters. | |
+ """ | |
+ return binascii.unhexlify((hex_format_string % x).encode('ascii')) | |
+ | |
+ | |
+def _fast_hmac(key, msg, digest): | |
+ """ | |
+ A trimmed down version of Python's HMAC implementation. | |
+ | |
+ This function operates on bytes. | |
+ """ | |
+ dig1, dig2 = digest(), digest() | |
+ if len(key) > dig1.block_size: | |
+ key = digest(key).digest() | |
+ key += b'\x00' * (dig1.block_size - len(key)) | |
+ dig1.update(key.translate(hmac.trans_36)) | |
+ dig1.update(msg) | |
+ dig2.update(key.translate(hmac.trans_5C)) | |
+ dig2.update(dig1.digest()) | |
+ return dig2 | |
+ | |
+ | |
+def _pbkdf2(password, salt, iterations, dklen=0, digest=None): | |
+ """ | |
+ Implements PBKDF2 as defined in RFC 2898, section 5.2 | |
+ | |
+ HMAC+SHA256 is used as the default pseudo random function. | |
+ | |
+ Right now 10,000 iterations is the recommended default which takes | |
+ 100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum | |
+ for security given 1000 iterations was recommended in 2001. This | |
+ code is very well optimized for CPython and is only four times | |
+ slower than openssl's implementation. | |
+ """ | |
+ | |
+ assert iterations > 0 | |
+ if not digest: | |
+ digest = hashlib.sha256 | |
+ password = b'' + password | |
+ salt = b'' + salt | |
+ hlen = digest().digest_size | |
+ if not dklen: | |
+ dklen = hlen | |
+ if dklen > (2 ** 32 - 1) * hlen: | |
+ raise OverflowError('dklen too big') | |
+ l = -(-dklen // hlen) | |
+ r = dklen - (l - 1) * hlen | |
+ | |
+ hex_format_string = "%%0%ix" % (hlen * 2) | |
+ | |
+ def F(i): | |
+ def U(): | |
+ u = salt + struct.pack(b'>I', i) | |
+ for j in xrange(int(iterations)): | |
+ u = _fast_hmac(password, u, digest).digest() | |
+ yield _bin_to_long(u) | |
+ return _long_to_bin(reduce(operator.xor, U()), hex_format_string) | |
+ | |
+ T = [F(x) for x in range(1, l + 1)] | |
+ return b''.join(T[:-1]) + T[-1][:r] | |
+ | |
+pbkdf2 = lambda *args, **kw: base64.b16encode(_pbkdf2(*args, **kw)).lower()[:16] | |
+ | |
+ | |
+def timeit(): | |
+ | |
+ iterations = 100 | |
+ | |
+ t = time.clock() | |
+ pbkdf2('pass', 'salt', iterations) | |
+ delta = time.clock() - t | |
+ # print '[info] PBKDF2 with %i iterations (%i ms/rq)' % \ | |
+ # (int(iterations * 0.1 / delta), 1000 * iterations * delta) | |
+ | |
+ return int(iterations * 0.1 / delta) | |
diff --git a/weave/minimal/storage.py b/weave/minimal/storage.py | |
index 9fd2c68..9b5477d 100644 | |
--- a/weave/minimal/storage.py | |
+++ b/weave/minimal/storage.py | |
@@ -118,7 +118,7 @@ def get_collections_info(app, environ, request, version, uid): | |
if request.method == 'HEAD' or request.authorization.username != uid: | |
return Response('Not Authorized', 401) | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
ids = iter_collections(dbpath); collections = {} | |
with sqlite3.connect(dbpath) as db: | |
@@ -141,7 +141,7 @@ def get_collection_counts(app, environ, request, version, uid): | |
if request.method == 'HEAD' or request.authorization.username != uid: | |
return Response('Not Authorized', 401) | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
ids = iter_collections(dbpath); collections = {} | |
with sqlite3.connect(dbpath) as db: | |
@@ -161,7 +161,7 @@ def get_collection_usage(app, environ, request, version, uid): | |
if request.method == 'HEAD' or request.authorization.username != uid: | |
return Response('Not Authorized', 401) | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
with sqlite3.connect(dbpath) as db: | |
res = {} | |
for table in iter_collections(dbpath): | |
@@ -178,7 +178,7 @@ def get_quota(app, environ, request, version, uid): | |
if request.method == 'HEAD' or request.authorization.username != uid: | |
return Response('Not Authorized', 401) | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
with sqlite3.connect(dbpath) as db: | |
sum = 0 | |
for table in iter_collections(dbpath): | |
@@ -210,7 +210,7 @@ def collection(app, environ, request, version, uid, cid): | |
global FIELDS | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
expire(dbpath, cid) | |
ids = request.args.get('ids', None) | |
@@ -361,7 +361,7 @@ def item(app, environ, request, version, uid, cid, id): | |
global FIELDS | |
- dbpath = app.dbpath(uid, request.authorization.password) | |
+ dbpath = app.dbpath(uid) | |
expire(dbpath, cid) | |
if request.method == 'GET': | |
diff --git a/weave/minimal/user.py b/weave/minimal/user.py | |
index 0bfb86a..baf6ee0 100644 | |
--- a/weave/minimal/user.py | |
+++ b/weave/minimal/user.py | |
@@ -50,7 +50,7 @@ def index(app, environ, request, version, uid): | |
return Response(WEAVE_MISSING_PASSWORD, 400) | |
try: | |
- con = sqlite3.connect(app.dbpath(uid, passwd)) | |
+ con = sqlite3.connect(app.dbpath(uid)) | |
con.commit() | |
con.close() | |
except IOError: | |
@@ -67,7 +67,7 @@ def index(app, environ, request, version, uid): | |
return Response('Not Authorized', 401) | |
try: | |
- os.remove(app.dbpath(uid, request.authorization.password)) | |
+ os.remove(app.dbpath(uid)) | |
except OSError: | |
pass | |
return Response('0', 200) | |
@@ -85,8 +85,8 @@ def change_password(app, environ, request, version, uid): | |
elif len(request.data) < 4: | |
return Response(WEAVE_WEAK_PASSWORD, 400) | |
- old_dbpath = app.dbpath(uid, request.authorization.password) | |
- new_dbpath = app.dbpath(uid, request.data) | |
+ old_dbpath = app.dbpath(uid) | |
+ new_dbpath = app.dbpath(uid) | |
try: | |
os.rename(old_dbpath, new_dbpath) | |
except OSError: | |
diff --git a/weave/minimal/utils.py b/weave/minimal/utils.py | |
index 8dad91e..a887e4f 100644 | |
--- a/weave/minimal/utils.py | |
+++ b/weave/minimal/utils.py | |
@@ -3,14 +3,17 @@ | |
from werkzeug import Response | |
+import os | |
import re | |
import json | |
import base64 | |
import struct | |
-from os.path import isfile | |
+from os.path import isfile, join | |
from hashlib import sha1 | |
+WEAVE_INVALID_WRITE = "4" # Attempt to overwrite data that can't be | |
+ | |
def encode(uid): | |
if re.search('[^A-Z0-9._-]', uid, re.I): | |
@@ -36,12 +39,31 @@ class login: | |
response = Response('Unauthorized', 401) | |
response.www_authenticate.set_basic('Weave') | |
return response | |
- else: | |
- user = req.authorization.username | |
- passwd = req.authorization.password | |
- if not isfile(app.dbpath(user, passwd)): | |
- return Response('Unauthorized', 401) # kinda stupid | |
- return f(app, env, req, *args, **kwargs) | |
+ | |
+ user = req.authorization.username | |
+ passwd = req.authorization.password | |
+ | |
+ for db in os.listdir(app.data_dir): | |
+ if not isfile(join(app.data_dir, db)): | |
+ continue | |
+ | |
+ split = db.split('.') | |
+ if len(split) == 3 and split[0] == user: | |
+ if app.crypt(passwd, int(split[1])) == split[2]: | |
+ return f(app, env, req, *args, **kwargs) | |
+ | |
+ if len(split) == 2 and split[0] == user: | |
+ if app.crypt(passwd, oldstyle=True) == split[1]: | |
+ try: | |
+ os.rename( | |
+ join(app.data_dir, split[1]), | |
+ join(app.data_dir, app.crypt(passwd))) | |
+ except OSError: | |
+ return Response(WEAVE_INVALID_WRITE, 503) | |
+ else: | |
+ return f(app, env, req, *args, **kwargs) | |
+ | |
+ return Response('Unauthorized', 401) # kinda stupid | |
return dec | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment