"""
passlib.utils.binary - binary data encoding/decoding/manipulation
"""
#=============================================================================
# imports
#=============================================================================
# core
from __future__ import absolute_import, division, print_function
from base64 import (
b64encode,
b64decode,
b32decode as _b32decode,
b32encode as _b32encode,
)
from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError
import logging
log = logging.getLogger(__name__)
# site
# pkg
from passlib import exc
from passlib.utils.compat import (
PY3, bascii_to_str,
irange, imap, iter_byte_chars, join_byte_values, join_byte_elems,
nextgetter, suppress_cause,
u, unicode, unicode_or_bytes_types,
)
from passlib.utils.decor import memoized_property
# from passlib.utils import BASE64_CHARS, HASH64_CHARS
# local
__all__ = [
# constants
"BASE64_CHARS", "PADDED_BASE64_CHARS",
"AB64_CHARS",
"HASH64_CHARS",
"BCRYPT_CHARS",
"HEX_CHARS", "LOWER_HEX_CHARS", "UPPER_HEX_CHARS",
"ALL_BYTE_VALUES",
# misc
"compile_byte_translation",
# base64
'ab64_encode', 'ab64_decode',
'b64s_encode', 'b64s_decode',
# base32
"b32encode", "b32decode",
# custom encodings
'Base64Engine',
'LazyBase64Engine',
'h64',
'h64big',
'bcrypt64',
]
#=============================================================================
# constant strings
#=============================================================================
#-------------------------------------------------------------
# common salt_chars & checksum_chars values
#-------------------------------------------------------------
#: standard base64 charmap
BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
#: alt base64 charmap -- "." instead of "+"
AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./")
#: charmap used by HASH64 encoding.
HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
#: charmap used by BCrypt
BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
#: std base64 chars + padding char
PADDED_BASE64_CHARS = BASE64_CHARS + u("=")
#: all hex chars
HEX_CHARS = u("0123456789abcdefABCDEF")
#: upper case hex chars
UPPER_HEX_CHARS = u("0123456789ABCDEF")
#: lower case hex chars
LOWER_HEX_CHARS = u("0123456789abcdef")
#-------------------------------------------------------------
# byte strings
#-------------------------------------------------------------
#: special byte string containing all possible byte values
#: NOTE: for efficiency, this is treated as singleton by some of the code
ALL_BYTE_VALUES = join_byte_values(irange(256))
#: some string constants we reuse
B_EMPTY = b''
B_NULL = b'\x00'
B_EQUAL = b'='
#=============================================================================
# byte translation
#=============================================================================
#: base list used to compile byte translations
_TRANSLATE_SOURCE = list(iter_byte_chars(ALL_BYTE_VALUES))
def compile_byte_translation(mapping, source=None):
"""
return a 256-byte string for translating bytes using specified mapping.
bytes not specified by mapping will be left alone.
:param mapping:
dict mapping input byte (str or int) -> output byte (str or int).
:param source:
optional existing byte translation string to use as base.
(must be 255-length byte string). defaults to identity mapping.
:returns:
255-length byte string for passing to bytes().translate.
"""
if source is None:
target = _TRANSLATE_SOURCE[:]
else:
assert isinstance(source, bytes) and len(source) == 255
target = list(iter_byte_chars(source))
for k, v in mapping.items():
if isinstance(k, unicode_or_bytes_types):
k = ord(k)
assert isinstance(k, int) and 0 <= k < 256
if isinstance(v, unicode):
v = v.encode("ascii")
assert isinstance(v, bytes) and len(v) == 1
target[k] = v
return B_EMPTY.join(target)
#=============================================================================
# unpadding / stripped base64 encoding
#=============================================================================
def b64s_encode(data):
"""
encode using shortened base64 format which omits padding & whitespace.
uses default ``+/`` altchars.
"""
return b2a_base64(data).rstrip(_BASE64_STRIP)
def b64s_decode(data):
"""
decode from shortened base64 format which omits padding & whitespace.
uses default ``+/`` altchars.
"""
if isinstance(data, unicode):
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
try:
data = data.encode("ascii")
except UnicodeEncodeError:
raise suppress_cause(ValueError("string argument should contain only ASCII characters"))
off = len(data) & 3
if off == 0:
pass
elif off == 2:
data += _BASE64_PAD2
elif off == 3:
data += _BASE64_PAD1
else: # off == 1
raise ValueError("invalid base64 input")
try:
return a2b_base64(data)
except _BinAsciiError as err:
raise suppress_cause(TypeError(err))
#=============================================================================
# adapted-base64 encoding
#=============================================================================
_BASE64_STRIP = b"=\n"
_BASE64_PAD1 = b"="
_BASE64_PAD2 = b"=="
# XXX: Passlib 1.8/1.9 -- deprecate everything that's using ab64_encode(),
# have it start outputing b64s_encode() instead? can use a64_decode() to retain backwards compat.
def ab64_encode(data):
"""
encode using shortened base64 format which omits padding & whitespace.
uses custom ``./`` altchars.
it is primarily used by Passlib's custom pbkdf2 hashes.
"""
return b64s_encode(data).replace(b"+", b".")
def ab64_decode(data):
"""
decode from shortened base64 format which omits padding & whitespace.
uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well.
it is primarily used by Passlib's custom pbkdf2 hashes.
"""
if isinstance(data, unicode):
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
try:
data = data.encode("ascii")
except UnicodeEncodeError:
raise suppress_cause(ValueError("string argument should contain only ASCII characters"))
return b64s_decode(data.replace(b".", b"+"))
#=============================================================================
# base32 codec
#=============================================================================
def b32encode(source):
"""
wrapper around :func:`base64.b32encode` which strips padding,
and returns a native string.
"""
# NOTE: using upper case by default here, since 'I & L' are less
# visually ambiguous than 'i & l'
return bascii_to_str(_b32encode(source).rstrip(B_EQUAL))
#: byte translation map to replace common mistyped base32 chars.
#: XXX: could correct '1' -> 'I', but could be a mistyped lower-case 'l', so leaving it alone.
_b32_translate = compile_byte_translation({"8": "B", "0": "O"})
#: helper to add padding
_b32_decode_pad = B_EQUAL * 8
def b32decode(source):
"""
wrapper around :func:`base64.b32decode`
which handles common mistyped chars.
padding optional, ignored if present.
"""
# encode & correct for typos
if isinstance(source, unicode):
source = source.encode("ascii")
source = source.translate(_b32_translate)
# pad things so final string is multiple of 8
remainder = len(source) & 0x7
if remainder:
source += _b32_decode_pad[:-remainder]
# XXX: py27 stdlib's version of this has some inefficiencies,
# could look into using optimized version.
return _b32decode(source, True)
#=============================================================================
# base64-variant encoding
#=============================================================================
class Base64Engine(object):
"""Provides routines for encoding/decoding base64 data using
arbitrary character mappings, selectable endianness, etc.
:arg charmap:
A string of 64 unique characters,
which will be used to encode successive 6-bit chunks of data.
A character's position within the string should correspond
to its 6-bit value.
:param big:
Whether the encoding should be big-endian (default False).
.. note::
This class does not currently handle base64's padding characters
in any way what so ever.
Raw Bytes <-> Encoded Bytes
===========================
The following methods convert between raw bytes,
and strings encoded using the engine's specific base64 variant:
.. automethod:: encode_bytes
.. automethod:: decode_bytes
.. automethod:: encode_transposed_bytes
.. automethod:: decode_transposed_bytes
..
.. automethod:: check_repair_unused
.. automethod:: repair_unused
Integers <-> Encoded Bytes
==========================
The following methods allow encoding and decoding
unsigned integers to and from the engine's specific base64 variant.
Endianess is determined by the engine's ``big`` constructor keyword.
.. automethod:: encode_int6
.. automethod:: decode_int6
.. automethod:: encode_int12
.. automethod:: decode_int12
.. automethod:: encode_int24
.. automethod:: decode_int24
.. automethod:: encode_int64
.. automethod:: decode_int64
Informational Attributes
========================
.. attribute:: charmap
unicode string containing list of characters used in encoding;
position in string matches 6bit value of character.
.. attribute:: bytemap
bytes version of :attr:`charmap`
.. attribute:: big
boolean flag indicating this using big-endian encoding.
"""
#===================================================================
# instance attrs
#===================================================================
# public config
bytemap = None # charmap as bytes
big = None # little or big endian
# filled in by init based on charmap.
# (byte elem: single byte under py2, 8bit int under py3)
_encode64 = None # maps 6bit value -> byte elem
_decode64 = None # maps byte elem -> 6bit value
# helpers filled in by init based on endianness
_encode_bytes = None # throws IndexError if bad value (shouldn't happen)
_decode_bytes = None # throws KeyError if bad char.
#===================================================================
# init
#===================================================================
def __init__(self, charmap, big=False):
# validate charmap, generate encode64/decode64 helper functions.
if isinstance(charmap, unicode):
charmap = charmap.encode("latin-1")
elif not isinstance(charmap, bytes):
raise exc.ExpectedStringError(charmap, "charmap")
if len(charmap) != 64:
raise ValueError("charmap must be 64 characters in length")
if len(set(charmap)) != 64:
raise ValueError("charmap must not contain duplicate characters")
self.bytemap = charmap
self._encode64 = charmap.__getitem__
lookup = dict((value, idx) for idx, value in enumerate(charmap))
self._decode64 = lookup.__getitem__
# validate big, set appropriate helper functions.
self.big = big
if big:
self._encode_bytes = self._encode_bytes_big
self._decode_bytes = self._decode_bytes_big
else:
self._encode_bytes = self._encode_bytes_little
self._decode_bytes = self._decode_bytes_little
# TODO: support padding character
##if padding is not None:
## if isinstance(padding, unicode):
## padding = padding.encode("latin-1")
## elif not isinstance(padding, bytes):
## raise TypeError("padding char must be unicode or bytes")
## if len(padding) != 1:
## raise ValueError("padding must be single character")
##self.padding = padding
@property
def charmap(self):
"""charmap as unicode"""
return self.bytemap.decode("latin-1")
#===================================================================
# encoding byte strings
#===================================================================
def encode_bytes(self, source):
"""encode bytes to base64 string.
:arg source: byte string to encode.
:returns: byte string containing encoded data.
"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
chunks, tail = divmod(len(source), 3)
if PY3:
next_value = nextgetter(iter(source))
else:
next_value = nextgetter(ord(elem) for elem in source)
gen = self._encode_bytes(next_value, chunks, tail)
out = join_byte_elems(imap(self._encode64, gen))
##if tail:
## padding = self.padding
## if padding:
## out += padding * (3-tail)
return out
def _encode_bytes_little(self, next_value, chunks, tail):
"""helper used by encode_bytes() to handle little-endian encoding"""
#
# output bit layout:
#
# first byte: v1 543210
#
# second byte: v1 ....76
# +v2 3210..
#
# third byte: v2 ..7654
# +v3 10....
#
# fourth byte: v3 765432
#
idx = 0
while idx < chunks:
v1 = next_value()
v2 = next_value()
v3 = next_value()
yield v1 & 0x3f
yield ((v2 & 0x0f)<<2)|(v1>>6)
yield ((v3 & 0x03)<<4)|(v2>>4)
yield v3>>2
idx += 1
if tail:
v1 = next_value()
if tail == 1:
# note: 4 msb of last byte are padding
yield v1 & 0x3f
yield v1>>6
else:
assert tail == 2
# note: 2 msb of last byte are padding
v2 = next_value()
yield v1 & 0x3f
yield ((v2 & 0x0f)<<2)|(v1>>6)
yield v2>>4
def _encode_bytes_big(self, next_value, chunks, tail):
"""helper used by encode_bytes() to handle big-endian encoding"""
#
# output bit layout:
#
# first byte: v1 765432
#
# second byte: v1 10....
# +v2 ..7654
#
# third byte: v2 3210..
# +v3 ....76
#
# fourth byte: v3 543210
#
idx = 0
while idx < chunks:
v1 = next_value()
v2 = next_value()
v3 = next_value()
yield v1>>2
yield ((v1&0x03)<<4)|(v2>>4)
yield ((v2&0x0f)<<2)|(v3>>6)
yield v3 & 0x3f
idx += 1
if tail:
v1 = next_value()
if tail == 1:
# note: 4 lsb of last byte are padding
yield v1>>2
yield (v1&0x03)<<4
else:
assert tail == 2
# note: 2 lsb of last byte are padding
v2 = next_value()
yield v1>>2
yield ((v1&0x03)<<4)|(v2>>4)
yield ((v2&0x0f)<<2)
#===================================================================
# decoding byte strings
#===================================================================
def decode_bytes(self, source):
"""decode bytes from base64 string.
:arg source: byte string to decode.
:returns: byte string containing decoded data.
"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
##padding = self.padding
##if padding:
## # TODO: add padding size check?
## source = source.rstrip(padding)
chunks, tail = divmod(len(source), 4)
if tail == 1:
# only 6 bits left, can't encode a whole byte!
raise ValueError("input string length cannot be == 1 mod 4")
next_value = nextgetter(imap(self._decode64, source))
try:
return join_byte_values(self._decode_bytes(next_value, chunks, tail))
except KeyError as err:
raise ValueError("invalid character: %r" % (err.args[0],))
def _decode_bytes_little(self, next_value, chunks, tail):
"""helper used by decode_bytes() to handle little-endian encoding"""
#
# input bit layout:
#
# first byte: v1 ..543210
# +v2 10......
#
# second byte: v2 ....5432
# +v3 3210....
#
# third byte: v3 ......54
# +v4 543210..
#
idx = 0
while idx < chunks:
v1 = next_value()
v2 = next_value()
v3 = next_value()
v4 = next_value()
yield v1 | ((v2 & 0x3) << 6)
yield (v2>>2) | ((v3 & 0xF) << 4)
yield (v3>>4) | (v4<<2)
idx += 1
if tail:
# tail is 2 or 3
v1 = next_value()
v2 = next_value()
yield v1 | ((v2 & 0x3) << 6)
# NOTE: if tail == 2, 4 msb of v2 are ignored (should be 0)
if tail == 3:
# NOTE: 2 msb of v3 are ignored (should be 0)
v3 = next_value()
yield (v2>>2) | ((v3 & 0xF) << 4)
def _decode_bytes_big(self, next_value, chunks, tail):
"""helper used by decode_bytes() to handle big-endian encoding"""
#
# input bit layout:
#
# first byte: v1 543210..
# +v2 ......54
#
# second byte: v2 3210....
# +v3 ....5432
#
# third byte: v3 10......
# +v4 ..543210
#
idx = 0
while idx < chunks:
v1 = next_value()
v2 = next_value()
v3 = next_value()
v4 = next_value()
yield (v1<<2) | (v2>>4)
yield ((v2&0xF)<<4) | (v3>>2)
yield ((v3&0x3)<<6) | v4
idx += 1
if tail:
# tail is 2 or 3
v1 = next_value()
v2 = next_value()
yield (v1<<2) | (v2>>4)
# NOTE: if tail == 2, 4 lsb of v2 are ignored (should be 0)
if tail == 3:
# NOTE: 2 lsb of v3 are ignored (should be 0)
v3 = next_value()
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]
assert last in padset, "failed to generate valid padding char"
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"
if PY3:
last = bytes([last])
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):
"""encode byte string, first transposing source using offset list"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
tmp = join_byte_elems(source[off] for off in offsets)
return self.encode_bytes(tmp)
def decode_transposed_bytes(self, source, offsets):
"""decode byte string, then reverse transposition described by offset list"""
# NOTE: if transposition does not use all bytes of source,
# the original can't be recovered... and join_byte_elems() will throw
# an error because 1+ values in <buf> will be None.
tmp = self.decode_bytes(source)
buf = [None] * len(offsets)
for off, char in zip(offsets, tmp):
buf[off] = char
return join_byte_elems(buf)
#===================================================================
# integer decoding helpers - mainly used by des_crypt family
#===================================================================
def _decode_int(self, source, bits):
"""decode base64 string -> integer
:arg source: base64 string to decode.
:arg bits: number of bits in resulting integer.
:raises ValueError:
* if the string contains invalid base64 characters.
* if the string is not long enough - it must be at least
``int(ceil(bits/6))`` in length.
:returns:
a integer in the range ``0 <= n < 2**bits``
"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
big = self.big
pad = -bits % 6
chars = (bits+pad)/6
if len(source) != chars:
raise ValueError("source must be %d chars" % (chars,))
decode = self._decode64
out = 0
try:
for c in source if big else reversed(source):
out = (out<<6) + decode(c)
except KeyError:
raise ValueError("invalid character in string: %r" % (c,))
if pad:
# strip padding bits
if big:
out >>= pad
else:
out &= (1<<bits)-1
return out
#---------------------------------------------------------------
# optimized versions for common integer sizes
#---------------------------------------------------------------
def decode_int6(self, source):
"""decode single character -> 6 bit integer"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
if len(source) != 1:
raise ValueError("source must be exactly 1 byte")
if PY3:
# convert to 8bit int before doing lookup
source = source[0]
try:
return self._decode64(source)
except KeyError:
raise ValueError("invalid character")
def decode_int12(self, source):
"""decodes 2 char string -> 12-bit integer"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
if len(source) != 2:
raise ValueError("source must be exactly 2 bytes")
decode = self._decode64
try:
if self.big:
return decode(source[1]) + (decode(source[0])<<6)
else:
return decode(source[0]) + (decode(source[1])<<6)
except KeyError:
raise ValueError("invalid character")
def decode_int24(self, source):
"""decodes 4 char string -> 24-bit integer"""
if not isinstance(source, bytes):
raise TypeError("source must be bytes, not %s" % (type(source),))
if len(source) != 4:
raise ValueError("source must be exactly 4 bytes")
decode = self._decode64
try:
if self.big:
return decode(source[3]) + (decode(source[2])<<6)+ \
(decode(source[1])<<12) + (decode(source[0])<<18)
else:
return decode(source[0]) + (decode(source[1])<<6)+ \
(decode(source[2])<<12) + (decode(source[3])<<18)
except KeyError:
raise ValueError("invalid character")
def decode_int30(self, source):
"""decode 5 char string -> 30 bit integer"""
return self._decode_int(source, 30)
def decode_int64(self, source):
"""decode 11 char base64 string -> 64-bit integer
this format is used primarily by des-crypt & variants to encode
the DES output value used as a checksum.
"""
return self._decode_int(source, 64)
#===================================================================
# integer encoding helpers - mainly used by des_crypt family
#===================================================================
def _encode_int(self, value, bits):
"""encode integer into base64 format
:arg value: non-negative integer to encode
:arg bits: number of bits to encode
:returns:
a string of length ``int(ceil(bits/6.0))``.
"""
assert value >= 0, "caller did not sanitize input"
pad = -bits % 6
bits += pad
if self.big:
itr = irange(bits-6, -6, -6)
# shift to add lsb padding.
value <<= pad
else:
itr = irange(0, bits, 6)
# padding is msb, so no change needed.
return join_byte_elems(imap(self._encode64,
((value>>off) & 0x3f for off in itr)))
#---------------------------------------------------------------
# optimized versions for common integer sizes
#---------------------------------------------------------------
def encode_int6(self, value):
"""encodes 6-bit integer -> single hash64 character"""
if value < 0 or value > 63:
raise ValueError("value out of range")
if PY3:
return self.bytemap[value:value+1]
else:
return self._encode64(value)
def encode_int12(self, value):
"""encodes 12-bit integer -> 2 char string"""
if value < 0 or value > 0xFFF:
raise ValueError("value out of range")
raw = [value & 0x3f, (value>>6) & 0x3f]
if self.big:
raw = reversed(raw)
return join_byte_elems(imap(self._encode64, raw))
def encode_int24(self, value):
"""encodes 24-bit integer -> 4 char string"""
if value < 0 or value > 0xFFFFFF:
raise ValueError("value out of range")
raw = [value & 0x3f, (value>>6) & 0x3f,
(value>>12) & 0x3f, (value>>18) & 0x3f]
if self.big:
raw = reversed(raw)
return join_byte_elems(imap(self._encode64, raw))
def encode_int30(self, value):
"""decode 5 char string -> 30 bit integer"""
if value < 0 or value > 0x3fffffff:
raise ValueError("value out of range")
return self._encode_int(value, 30)
def encode_int64(self, value):
"""encode 64-bit integer -> 11 char hash64 string
this format is used primarily by des-crypt & variants to encode
the DES output value used as a checksum.
"""
if value < 0 or value > 0xffffffffffffffff:
raise ValueError("value out of range")
return self._encode_int(value, 64)
#===================================================================
# 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 variants
#-------------------------------------------------------------
h64 = LazyBase64Engine(HASH64_CHARS)
h64big = LazyBase64Engine(HASH64_CHARS, big=True)
bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True)
#=============================================================================
# eof
#=============================================================================