|
@@ -1,34 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
+
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
@@ -38,7 +39,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
@@ -47,54 +48,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-__version__ = "1.2"
|
|
|
+__version__ = "1.3"
|
|
|
+__all__ = ['PBKDF2', 'crypt']
|
|
|
|
|
|
from struct import pack
|
|
|
-from binascii import b2a_hex
|
|
|
from random import randint
|
|
|
import string
|
|
|
-import collections
|
|
|
+import sys
|
|
|
|
|
|
try:
|
|
|
-
|
|
|
+
|
|
|
from Crypto.Hash import HMAC, SHA as SHA1
|
|
|
-
|
|
|
except ImportError:
|
|
|
|
|
|
import hmac as HMAC
|
|
|
- import sha as SHA1
|
|
|
+ try:
|
|
|
+ from hashlib import sha1 as SHA1
|
|
|
+ except ImportError:
|
|
|
+
|
|
|
+ import sha as SHA1
|
|
|
|
|
|
-def strxor(a, b):
|
|
|
- return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
-def b64encode(data, chars="+/"):
|
|
|
- tt = string.maketrans("+/", chars)
|
|
|
- return data.encode('base64').replace("\n", "").translate(tt)
|
|
|
+if sys.version_info[0] == 2:
|
|
|
+ _0xffffffffL = long(1) << 32
|
|
|
+ def isunicode(s):
|
|
|
+ return isinstance(s, unicode)
|
|
|
+ def isbytes(s):
|
|
|
+ return isinstance(s, str)
|
|
|
+ def isinteger(n):
|
|
|
+ return isinstance(n, (int, long))
|
|
|
+ def b(s):
|
|
|
+ return s
|
|
|
+ def binxor(a, b):
|
|
|
+ return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])
|
|
|
+ def b64encode(data, chars="+/"):
|
|
|
+ tt = string.maketrans("+/", chars)
|
|
|
+ return data.encode('base64').replace("\n", "").translate(tt)
|
|
|
+ from binascii import b2a_hex
|
|
|
+else:
|
|
|
+ _0xffffffffL = 0xffffffff
|
|
|
+ def isunicode(s):
|
|
|
+ return isinstance(s, str)
|
|
|
+ def isbytes(s):
|
|
|
+ return isinstance(s, bytes)
|
|
|
+ def isinteger(n):
|
|
|
+ return isinstance(n, int)
|
|
|
+ def callable(obj):
|
|
|
+ return hasattr(obj, '__call__')
|
|
|
+ def b(s):
|
|
|
+ return s.encode("latin-1")
|
|
|
+ def binxor(a, b):
|
|
|
+ return bytes([x ^ y for (x, y) in zip(a, b)])
|
|
|
+ from base64 import b64encode as _b64encode
|
|
|
+ def b64encode(data, chars="+/"):
|
|
|
+ if isunicode(chars):
|
|
|
+ return _b64encode(data, chars.encode('utf-8')).decode('utf-8')
|
|
|
+ else:
|
|
|
+ return _b64encode(data, chars)
|
|
|
+ from binascii import b2a_hex as _b2a_hex
|
|
|
+ def b2a_hex(s):
|
|
|
+ return _b2a_hex(s).decode('us-ascii')
|
|
|
+ xrange = range
|
|
|
|
|
|
class PBKDF2(object):
|
|
|
"""PBKDF2.py : PKCS#5 v2.0 Password-Based Key Derivation
|
|
|
-
|
|
|
+
|
|
|
This implementation takes a passphrase and a salt (and optionally an
|
|
|
iteration count, a digest module, and a MAC module) and provides a
|
|
|
file-like object from which an arbitrarily-sized key can be read.
|
|
@@ -104,10 +125,10 @@ class PBKDF2(object):
|
|
|
|
|
|
The idea behind PBKDF2 is to derive a cryptographic key from a
|
|
|
passphrase and a salt.
|
|
|
-
|
|
|
+
|
|
|
PBKDF2 may also be used as a strong salted password hash. The
|
|
|
'crypt' function is provided for that purpose.
|
|
|
-
|
|
|
+
|
|
|
Remember: Keys generated using PBKDF2 are only as strong as the
|
|
|
passphrases they are derived from.
|
|
|
"""
|
|
@@ -122,7 +143,7 @@ class PBKDF2(object):
|
|
|
"""Pseudorandom function. e.g. HMAC-SHA1"""
|
|
|
return self.__macmodule.new(key=key, msg=msg,
|
|
|
digestmod=self.__digestmodule).digest()
|
|
|
-
|
|
|
+
|
|
|
def read(self, bytes):
|
|
|
"""Read the specified number of key bytes."""
|
|
|
if self.closed:
|
|
@@ -133,28 +154,28 @@ class PBKDF2(object):
|
|
|
i = self.__blockNum
|
|
|
while size < bytes:
|
|
|
i += 1
|
|
|
- if i > 0xffffffff or i < 1:
|
|
|
-
|
|
|
+ if i > _0xffffffffL or i < 1:
|
|
|
+
|
|
|
raise OverflowError("derived key too long")
|
|
|
block = self.__f(i)
|
|
|
blocks.append(block)
|
|
|
size += len(block)
|
|
|
- buf = "".join(blocks)
|
|
|
+ buf = b("").join(blocks)
|
|
|
retval = buf[:bytes]
|
|
|
self.__buf = buf[bytes:]
|
|
|
self.__blockNum = i
|
|
|
return retval
|
|
|
-
|
|
|
+
|
|
|
def __f(self, i):
|
|
|
|
|
|
- assert 1 <= i <= 0xffffffff
|
|
|
+ assert 1 <= i <= _0xffffffffL
|
|
|
U = self.__prf(self.__passphrase, self.__salt + pack("!L", i))
|
|
|
result = U
|
|
|
- for j in range(2, 1+self.__iterations):
|
|
|
+ for j in xrange(2, 1+self.__iterations):
|
|
|
U = self.__prf(self.__passphrase, U)
|
|
|
- result = strxor(result, U)
|
|
|
+ result = binxor(result, U)
|
|
|
return result
|
|
|
-
|
|
|
+
|
|
|
def hexread(self, octets):
|
|
|
"""Read the specified number of octets. Return them as hexadecimal.
|
|
|
|
|
@@ -164,26 +185,26 @@ class PBKDF2(object):
|
|
|
|
|
|
def _setup(self, passphrase, salt, iterations, prf):
|
|
|
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
- if isinstance(passphrase, str):
|
|
|
+ if isunicode(passphrase):
|
|
|
passphrase = passphrase.encode("UTF-8")
|
|
|
- if not isinstance(passphrase, str):
|
|
|
+ elif not isbytes(passphrase):
|
|
|
raise TypeError("passphrase must be str or unicode")
|
|
|
- if isinstance(salt, str):
|
|
|
+ if isunicode(salt):
|
|
|
salt = salt.encode("UTF-8")
|
|
|
- if not isinstance(salt, str):
|
|
|
+ elif not isbytes(salt):
|
|
|
raise TypeError("salt must be str or unicode")
|
|
|
|
|
|
|
|
|
- if not isinstance(iterations, int):
|
|
|
+ if not isinteger(iterations):
|
|
|
raise TypeError("iterations must be an integer")
|
|
|
if iterations < 1:
|
|
|
raise ValueError("iterations must be at least 1")
|
|
|
-
|
|
|
+
|
|
|
|
|
|
- if not isinstance(prf, collections.Callable):
|
|
|
+ if not callable(prf):
|
|
|
raise TypeError("prf must be callable")
|
|
|
|
|
|
self.__passphrase = passphrase
|
|
@@ -191,9 +212,9 @@ class PBKDF2(object):
|
|
|
self.__iterations = iterations
|
|
|
self.__prf = prf
|
|
|
self.__blockNum = 0
|
|
|
- self.__buf = ""
|
|
|
+ self.__buf = b("")
|
|
|
self.closed = False
|
|
|
-
|
|
|
+
|
|
|
def close(self):
|
|
|
"""Close the stream."""
|
|
|
if not self.closed:
|
|
@@ -207,27 +228,29 @@ class PBKDF2(object):
|
|
|
|
|
|
def crypt(word, salt=None, iterations=None):
|
|
|
"""PBKDF2-based unix crypt(3) replacement.
|
|
|
-
|
|
|
+
|
|
|
The number of iterations specified in the salt overrides the 'iterations'
|
|
|
parameter.
|
|
|
|
|
|
The effective hash length is 192 bits.
|
|
|
"""
|
|
|
-
|
|
|
+
|
|
|
|
|
|
if salt is None:
|
|
|
salt = _makesalt()
|
|
|
|
|
|
|
|
|
- if isinstance(salt, str):
|
|
|
- salt = salt.encode("us-ascii")
|
|
|
- if not isinstance(salt, str):
|
|
|
+ if isunicode(salt):
|
|
|
+ salt = salt.encode('us-ascii').decode('us-ascii')
|
|
|
+ elif isbytes(salt):
|
|
|
+ salt = salt.decode('us-ascii')
|
|
|
+ else:
|
|
|
raise TypeError("salt must be a string")
|
|
|
|
|
|
|
|
|
- if isinstance(word, str):
|
|
|
+ if isunicode(word):
|
|
|
word = word.encode("UTF-8")
|
|
|
- if not isinstance(word, str):
|
|
|
+ elif not isbytes(word):
|
|
|
raise TypeError("word must be a string or unicode")
|
|
|
|
|
|
|
|
@@ -242,7 +265,7 @@ def crypt(word, salt=None, iterations=None):
|
|
|
iterations = converted
|
|
|
if not (iterations >= 1):
|
|
|
raise ValueError("Invalid salt")
|
|
|
-
|
|
|
+
|
|
|
|
|
|
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
|
|
|
for ch in salt:
|
|
@@ -264,92 +287,126 @@ PBKDF2.crypt = staticmethod(crypt)
|
|
|
|
|
|
def _makesalt():
|
|
|
"""Return a 48-bit pseudorandom salt for crypt().
|
|
|
-
|
|
|
+
|
|
|
This function is not suitable for generating cryptographic secrets.
|
|
|
"""
|
|
|
- binarysalt = "".join([pack("@H", randint(0, 0xffff)) for i in range(3)])
|
|
|
+ binarysalt = b("").join([pack("@H", randint(0, 0xffff)) for i in range(3)])
|
|
|
return b64encode(binarysalt, "./")
|
|
|
-
|
|
|
-def test_pbkdf2():
|
|
|
- """Module self-test"""
|
|
|
- from binascii import a2b_hex
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1).read(16)
|
|
|
- expected = a2b_hex("cdedb5281bb2f801565a1122b2563515")
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1200).hexread(32)
|
|
|
- expected = ("5c08eb61fdf71e4e4ec3cf6ba1f5512b"
|
|
|
- "a7e52ddbc5e5142f708a31e2e62b1e13")
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = PBKDF2("X"*64, "pass phrase equals block size", 1200).hexread(32)
|
|
|
- expected = ("139c30c0966bc32ba55fdbf212530ac9"
|
|
|
- "c5ec59f1a452f5cc9ad940fea0598ed1")
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = PBKDF2("X"*65, "pass phrase exceeds block size", 1200).hexread(32)
|
|
|
- expected = ("9ccad6d468770cd51b10e6a68721be61"
|
|
|
- "1a8b4d282601db3b36be9246915ec82a")
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- f = PBKDF2("kickstart", "workbench", 256)
|
|
|
- result = f.read(17)
|
|
|
- result += f.read(17)
|
|
|
- result += f.read(1)
|
|
|
- result += f.read(2)
|
|
|
- result += f.read(3)
|
|
|
- expected = PBKDF2("kickstart", "workbench", 256).read(40)
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- result = crypt("cloadm", "exec")
|
|
|
- expected = '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = crypt("gnu", '$p5k2$c$u9HvcT4d$.....')
|
|
|
- expected = '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = crypt("dcl", "tUsch7fU", iterations=13)
|
|
|
- expected = "$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL"
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
-
|
|
|
-
|
|
|
- result = crypt('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2',
|
|
|
- '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
|
|
|
- expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
|
|
|
- if result != expected:
|
|
|
- raise RuntimeError("self-test failed")
|
|
|
+
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
- test_pbkdf2()
|
|
|
+ import unittest
|
|
|
+
|
|
|
+
|
|
|
+ class TestPBKDF2(unittest.TestCase):
|
|
|
+ def test_pbkdf2(self):
|
|
|
+ """Module self-test"""
|
|
|
+ from binascii import a2b_hex as _a2b_hex
|
|
|
+ def a2b_hex(s):
|
|
|
+ return _a2b_hex(b(s))
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1).read(16)
|
|
|
+ expected = a2b_hex("cdedb5281bb2f801565a1122b2563515")
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1200).hexread(32)
|
|
|
+ expected = ("5c08eb61fdf71e4e4ec3cf6ba1f5512b"
|
|
|
+ "a7e52ddbc5e5142f708a31e2e62b1e13")
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = PBKDF2("X"*64, "pass phrase equals block size", 1200).hexread(32)
|
|
|
+ expected = ("139c30c0966bc32ba55fdbf212530ac9"
|
|
|
+ "c5ec59f1a452f5cc9ad940fea0598ed1")
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = PBKDF2("X"*65, "pass phrase exceeds block size", 1200).hexread(32)
|
|
|
+ expected = ("9ccad6d468770cd51b10e6a68721be61"
|
|
|
+ "1a8b4d282601db3b36be9246915ec82a")
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ f = PBKDF2("kickstart", "workbench", 256)
|
|
|
+ result = f.read(17)
|
|
|
+ result += f.read(17)
|
|
|
+ result += f.read(1)
|
|
|
+ result += f.read(2)
|
|
|
+ result += f.read(3)
|
|
|
+ expected = PBKDF2("kickstart", "workbench", 256).read(40)
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("cloadm", "exec")
|
|
|
+ expected = '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("gnu", '$p5k2$c$u9HvcT4d$.....')
|
|
|
+ expected = '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("dcl", "tUsch7fU", iterations=13)
|
|
|
+ expected = "$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL"
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt(b('\xce\x99\xcf\x89\xce\xb1\xce\xbd\xce\xbd\xce\xb7\xcf\x82').decode('utf-8'),
|
|
|
+ '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
|
|
|
+ expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt(b('\xce\x99\xcf\x89\xce\xb1\xce\xbd\xce\xbd\xce\xb7\xcf\x82'),
|
|
|
+ '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
|
|
|
+ expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+ def test_crypt(self):
|
|
|
+ result = crypt("secret")
|
|
|
+ self.assertEqual(result[:6], "$p5k2$")
|
|
|
+
|
|
|
+ result = crypt("secret", "XXXXXXXX")
|
|
|
+ expected = '$p5k2$$XXXXXXXX$L9mVVdq7upotdvtGvXTDTez3FIu3z0uG'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("secret", "XXXXXXXX", 400)
|
|
|
+ expected = '$p5k2$$XXXXXXXX$L9mVVdq7upotdvtGvXTDTez3FIu3z0uG'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("spam", "FRsH3HJB", iterations=400)
|
|
|
+ expected = '$p5k2$$FRsH3HJB$SgRWDNmB2LukCy0OTal6LYLHZVgtOi7s'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ result = crypt("spam", "H0NX9mT/", iterations=1000)
|
|
|
+ expected = '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ expected = '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'
|
|
|
+ result = crypt("spam", expected)
|
|
|
+ self.assertEqual(expected, result)
|
|
|
+
|
|
|
+
|
|
|
+ unittest.main(verbosity=2)
|
|
|
|
|
|
|