Skip to content

Instantly share code, notes, and snippets.

@posativ
Created August 8, 2013 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save posativ/abfc30a6ef3097d159ed to your computer and use it in GitHub Desktop.
Save posativ/abfc30a6ef3097d159ed to your computer and use it in GitHub Desktop.
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