diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index e1dc38a..3c00a03 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -375,6 +375,9 @@ class TestComments(unittest.TestCase): # just for the record self.assertEqual(self.post('/id/1/dislike', content_type=js).status_code, 200) + def testPBKDF2(self): + self.assertEqual(comments.API.pbkdf2(u"", Isso.salt, 1000, 6), u"42476aafe2e4") + class TestModeratedComments(unittest.TestCase): diff --git a/isso/utils/crypto.py b/isso/utils/crypto.py deleted file mode 100644 index bd812d3..0000000 --- a/isso/utils/crypto.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright (c) Django Software Foundation and individual contributors. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# 3. Neither the name of Django nor the names of its contributors may be used -# to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import hmac -import struct -import base64 -import hashlib -import binascii -import operator - -from functools import reduce - - -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.sha1 - 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 range(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 text, salt, iterations, dklen: base64.b16encode( - _pbkdf2(text.encode('utf-8'), salt, iterations, dklen)).lower() diff --git a/isso/views/comments.py b/isso/views/comments.py index 6c79189..6b25d22 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -9,17 +9,17 @@ import functools from itsdangerous import SignatureExpired, BadSignature from werkzeug.http import dump_cookie -from werkzeug.routing import Rule -from werkzeug.wrappers import Response -from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.wsgi import get_current_url from werkzeug.utils import redirect +from werkzeug.routing import Rule +from werkzeug.security import pbkdf2_hex +from werkzeug.wrappers import Response +from werkzeug.exceptions import BadRequest, Forbidden, NotFound from isso.compat import text_type as str from isso import utils, local from isso.utils import http, parse, JSONResponse as JSON -from isso.utils.crypto import pbkdf2 from isso.views import requires # from Django appearently, looks good to me *duck* @@ -139,6 +139,11 @@ class API(object): return True, "" + @classmethod + def pbkdf2(cls, text, salt, iterations, dklen): + # werkzeug.security.pbkdf2_hex returns always the native string type + return pbkdf2_hex(text.encode("utf-8"), salt, iterations, dklen) + @xhr @requires(str, 'uri') def new(self, environ, request, uri): @@ -197,7 +202,7 @@ class API(object): max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) - rv["hash"] = pbkdf2(rv['email'] or rv['remote_addr'], self.isso.salt, 1000, 6).decode("utf-8") + rv["hash"] = API.pbkdf2(rv['email'] or rv['remote_addr'], self.isso.salt, 1000, 6) self.cache.set('hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) @@ -429,7 +434,7 @@ class API(object): val = self.cache.get('hash', key.encode('utf-8')) if val is None: - val = pbkdf2(key, self.isso.salt, 1000, 6).decode("utf-8") + val = API.pbkdf2(key, self.isso.salt, 1000, 6) self.cache.set('hash', key.encode('utf-8'), val) item['hash'] = val