summaryrefslogtreecommitdiff
path: root/passlib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'passlib/utils')
-rw-r--r--passlib/utils/__init__.py124
-rw-r--r--passlib/utils/_blowfish/__init__.py13
2 files changed, 130 insertions, 7 deletions
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index b570480..051c93a 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -19,7 +19,7 @@ from warnings import warn
#pkg
from passlib.utils.compat import _add_doc, b, bytes, bjoin, bjoin_ints, \
bjoin_elems, exc_err, irange, imap, PY3, u, \
- ujoin, unicode
+ ujoin, unicode, belem_ord
#local
__all__ = [
# constants
@@ -202,6 +202,24 @@ def relocated_function(target, msg=None, name=None, deprecated=None, mod=None,
wrapper.__doc__ = msg
return wrapper
+class memoized_property(object):
+ """decorator which invokes method once, then replaces attr with result"""
+ def __init__(self, func):
+ self.im_func = func
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self
+ func = self.im_func
+ value = func(obj)
+ setattr(obj, func.__name__, value)
+ return value
+
+ @property
+ def __func__(self):
+ "py3 alias"
+ return self.im_func
+
#works but not used
##class memoized_class_property(object):
## """function decorator which calls function as classmethod,
@@ -597,6 +615,7 @@ class Base64Engine(object):
.. automethod:: decode_bytes
.. automethod:: encode_transposed_bytes
.. automethod:: decode_transposed_bytes
+ .. automethod:: check_repair_unused
Integers <-> Encoded Bytes
==========================
@@ -887,6 +906,86 @@ class Base64Engine(object):
yield ((v2&0xF)<<4) | (v3>>2)
#=============================================================
+ # encode/decode helpers
+ #=============================================================
+
+ # padmap2/3 - dict mapping last char of string ->
+ # equivalent char with no padding bits set.
+
+ def __make_padset(self, bits):
+ "helper to generate set of valid last chars & bytes"
+ pset = set(c for i,c in enumerate(self.bytemap) if not i & bits)
+ pset.update(c for i,c in enumerate(self.charmap) if not i & bits)
+ return frozenset(pset)
+
+ @memoized_property
+ def _padinfo2(self):
+ "mask to clear padding bits, and valid last bytes (for strings 2 % 4)"
+ # 4 bits of last char unused (lsb for big, msb for little)
+ bits = 15 if self.big else (15<<2)
+ return ~bits, self.__make_padset(bits)
+
+ @memoized_property
+ def _padinfo3(self):
+ "mask to clear padding bits, and valid last bytes (for strings 3 % 4)"
+ # 2 bits of last char unused (lsb for big, msb for little)
+ bits = 3 if self.big else (3<<4)
+ return ~bits, self.__make_padset(bits)
+
+ def check_repair_unused(self, source):
+ """helper to detect & clear invalid unused bits in last character.
+
+ :arg source:
+ encoded data (as ascii bytes or unicode).
+
+ :returns:
+ `(True, result)` if the string was repaired,
+ `(False, source)` if the string was ok as-is.
+ """
+ # figure out how many padding bits there are in last char.
+ tail = len(source) & 3
+ if tail == 2:
+ mask, padset = self._padinfo2
+ elif tail == 3:
+ mask, padset = self._padinfo3
+ elif not tail:
+ return False, source
+ else:
+ raise ValueError("source length must != 1 mod 4")
+
+ # check if last char is ok (padset contains bytes & unicode versions)
+ last = source[-1]
+ if last in padset:
+ return False, source
+
+ # we have dirty bits - repair the string by decoding last char,
+ # clearing the padding bits via <mask>, and encoding new char.
+ if isinstance(source, unicode):
+ cm = self.charmap
+ last = cm[cm.index(last) & mask]
+ else:
+ # NOTE: this assumes ascii-compat encoding, and that
+ # all chars used by encoding are 7-bit ascii.
+ last = self._encode64(self._decode64(last) & mask)
+ assert last in padset, "failed to generate valid padding char"
+ return True, source[:-1] + last
+
+ def repair_unused(self, source):
+ return self.check_repair_unused(source)[1]
+
+ ##def transcode(self, source, other):
+ ## return ''.join(
+ ## other.charmap[self.charmap.index(char)]
+ ## for char in source
+ ## )
+
+ ##def random_encoded_bytes(self, size, random=None, unicode=False):
+ ## "return random encoded string of given size"
+ ## data = getrandstr(random or rng,
+ ## self.charmap if unicode else self.bytemap, size)
+ ## return self.repair_unused(data)
+
+ #=============================================================
# transposed encoding/decoding
#=============================================================
def encode_transposed_bytes(self, source, offsets):
@@ -1076,6 +1175,24 @@ class Base64Engine(object):
# eof
#=============================================================
+class LazyBase64Engine(Base64Engine):
+ "Base64Engine which delays initialization until it's accessed"
+ _lazy_opts = None
+
+ def __init__(self, *args, **kwds):
+ self._lazy_opts = (args, kwds)
+
+ def _lazy_init(self):
+ args, kwds = self._lazy_opts
+ super(LazyBase64Engine, self).__init__(*args, **kwds)
+ del self._lazy_opts
+ self.__class__ = Base64Engine
+
+ def __getattribute__(self, attr):
+ if not attr.startswith("_"):
+ self._lazy_init()
+ return object.__getattribute__(self, attr)
+
# common charmaps
BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./")
@@ -1083,8 +1200,9 @@ HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx
BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
# common variants
-h64 = Base64Engine(HASH64_CHARS)
-h64big = Base64Engine(HASH64_CHARS, big=True)
+h64 = LazyBase64Engine(HASH64_CHARS)
+h64big = LazyBase64Engine(HASH64_CHARS, big=True)
+bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True)
#=============================================================================
# adapted-base64 encoding
diff --git a/passlib/utils/_blowfish/__init__.py b/passlib/utils/_blowfish/__init__.py
index 407b78f..d3444b8 100644
--- a/passlib/utils/_blowfish/__init__.py
+++ b/passlib/utils/_blowfish/__init__.py
@@ -54,7 +54,7 @@ released under the BSD license::
from itertools import chain
import struct
#pkg
-from passlib.utils import Base64Engine, BCRYPT_CHARS, getrandbytes, rng
+from passlib.utils import bcrypt64, getrandbytes, rng
from passlib.utils.compat import b, bytes, BytesIO, unicode, u
from passlib.utils._blowfish.unrolled import BlowfishEngine
#local
@@ -76,9 +76,6 @@ BCRYPT_CDATA = [
# struct used to encode ciphertext as digest (last output byte discarded)
digest_struct = struct.Struct(">6I")
-# base64 variant used by bcrypt
-bcrypt64 = Base64Engine(BCRYPT_CHARS, big=True)
-
#=========================================================
#base bcrypt helper
#
@@ -106,6 +103,14 @@ def raw_bcrypt(password, ident, salt, log_rounds):
minor = 0
elif ident == u('2a'):
minor = 1
+ # XXX: how to indicate caller wants to use crypt_blowfish's
+ # workaround variant of 2a?
+ elif ident == u('2x'):
+ raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
+ "currently supported")
+ elif ident == u('2y'):
+ # crypt_blowfish compatibility ident which guarantees compat w/ 2a
+ minor = 1
else:
raise ValueError("unknown ident: %r" % (ident,))