245 lines
8.7 KiB
Python
245 lines
8.7 KiB
Python
"""passlib.utils.scrypt._builtin -- scrypt() kdf in pure-python"""
|
|
#==========================================================================
|
|
# imports
|
|
#==========================================================================
|
|
# core
|
|
import operator
|
|
import struct
|
|
# pkg
|
|
from passlib.utils.compat import izip
|
|
from passlib.crypto.digest import pbkdf2_hmac
|
|
from passlib.crypto.scrypt._salsa import salsa20
|
|
# local
|
|
__all__ =[
|
|
"ScryptEngine",
|
|
]
|
|
|
|
#==========================================================================
|
|
# scrypt engine
|
|
#==========================================================================
|
|
class ScryptEngine(object):
|
|
"""
|
|
helper class used to run scrypt kdf, see scrypt() for frontend
|
|
|
|
.. warning::
|
|
this class does NO validation of the input ranges or types.
|
|
|
|
it's not intended to be used directly,
|
|
but only as a backend for :func:`passlib.utils.scrypt.scrypt()`.
|
|
"""
|
|
#=================================================================
|
|
# instance attrs
|
|
#=================================================================
|
|
|
|
# primary scrypt config parameters
|
|
n = 0
|
|
r = 0
|
|
p = 0
|
|
|
|
# derived values & objects
|
|
smix_bytes = 0
|
|
iv_bytes = 0
|
|
bmix_len = 0
|
|
bmix_half_len = 0
|
|
bmix_struct = None
|
|
integerify = None
|
|
|
|
#=================================================================
|
|
# frontend
|
|
#=================================================================
|
|
@classmethod
|
|
def execute(cls, secret, salt, n, r, p, keylen):
|
|
"""create engine & run scrypt() hash calculation"""
|
|
return cls(n, r, p).run(secret, salt, keylen)
|
|
|
|
#=================================================================
|
|
# init
|
|
#=================================================================
|
|
def __init__(self, n, r, p):
|
|
# store config
|
|
self.n = n
|
|
self.r = r
|
|
self.p = p
|
|
self.smix_bytes = r << 7 # num bytes in smix input - 2*r*16*4
|
|
self.iv_bytes = self.smix_bytes * p
|
|
self.bmix_len = bmix_len = r << 5 # length of bmix block list - 32*r integers
|
|
self.bmix_half_len = r << 4
|
|
assert struct.calcsize("I") == 4
|
|
self.bmix_struct = struct.Struct("<" + str(bmix_len) + "I")
|
|
|
|
# use optimized bmix for certain cases
|
|
if r == 1:
|
|
self.bmix = self._bmix_1
|
|
|
|
# pick best integerify function - integerify(bmix_block) should
|
|
# take last 64 bytes of block and return a little-endian integer.
|
|
# since it's immediately converted % n, we only have to extract
|
|
# the first 32 bytes if n < 2**32 - which due to the current
|
|
# internal representation, is already unpacked as a 32-bit int.
|
|
if n <= 0xFFFFffff:
|
|
integerify = operator.itemgetter(-16)
|
|
else:
|
|
assert n <= 0xFFFFffffFFFFffff
|
|
ig1 = operator.itemgetter(-16)
|
|
ig2 = operator.itemgetter(-17)
|
|
def integerify(X):
|
|
return ig1(X) | (ig2(X)<<32)
|
|
self.integerify = integerify
|
|
|
|
#=================================================================
|
|
# frontend
|
|
#=================================================================
|
|
def run(self, secret, salt, keylen):
|
|
"""
|
|
run scrypt kdf for specified secret, salt, and keylen
|
|
|
|
.. note::
|
|
|
|
* time cost is ``O(n * r * p)``
|
|
* mem cost is ``O(n * r)``
|
|
"""
|
|
# stretch salt into initial byte array via pbkdf2
|
|
iv_bytes = self.iv_bytes
|
|
input = pbkdf2_hmac("sha256", secret, salt, rounds=1, keylen=iv_bytes)
|
|
|
|
# split initial byte array into 'p' mflen-sized chunks,
|
|
# and run each chunk through smix() to generate output chunk.
|
|
smix = self.smix
|
|
if self.p == 1:
|
|
output = smix(input)
|
|
else:
|
|
# XXX: *could* use threading here, if really high p values encountered,
|
|
# but would tradeoff for more memory usage.
|
|
smix_bytes = self.smix_bytes
|
|
output = b''.join(
|
|
smix(input[offset:offset+smix_bytes])
|
|
for offset in range(0, iv_bytes, smix_bytes)
|
|
)
|
|
|
|
# stretch final byte array into output via pbkdf2
|
|
return pbkdf2_hmac("sha256", secret, output, rounds=1, keylen=keylen)
|
|
|
|
#=================================================================
|
|
# smix() helper
|
|
#=================================================================
|
|
def smix(self, input):
|
|
"""run SCrypt smix function on a single input block
|
|
|
|
:arg input:
|
|
byte string containing input data.
|
|
interpreted as 32*r little endian 4 byte integers.
|
|
|
|
:returns:
|
|
byte string containing output data
|
|
derived by mixing input using n & r parameters.
|
|
|
|
.. note:: time & mem cost are both ``O(n * r)``
|
|
"""
|
|
# gather locals
|
|
bmix = self.bmix
|
|
bmix_struct = self.bmix_struct
|
|
integerify = self.integerify
|
|
n = self.n
|
|
|
|
# parse input into 32*r integers ('X' in scrypt source)
|
|
# mem cost -- O(r)
|
|
buffer = list(bmix_struct.unpack(input))
|
|
|
|
# starting with initial buffer contents, derive V s.t.
|
|
# V[0]=initial_buffer ... V[i] = bmix(V[i-1], V[i-1]) ... V[n-1] = bmix(V[n-2], V[n-2])
|
|
# final buffer contents should equal bmix(V[n-1], V[n-1])
|
|
#
|
|
# time cost -- O(n * r) -- n loops, bmix is O(r)
|
|
# mem cost -- O(n * r) -- V is n-element array of r-element tuples
|
|
# NOTE: could do time / memory tradeoff to shrink size of V
|
|
def vgen():
|
|
i = 0
|
|
while i < n:
|
|
last = tuple(buffer)
|
|
yield last
|
|
bmix(last, buffer)
|
|
i += 1
|
|
V = list(vgen())
|
|
|
|
# generate result from X & V.
|
|
#
|
|
# time cost -- O(n * r) -- loops n times, calls bmix() which has O(r) time cost
|
|
# mem cost -- O(1) -- allocates nothing, calls bmix() which has O(1) mem cost
|
|
get_v_elem = V.__getitem__
|
|
n_mask = n - 1
|
|
i = 0
|
|
while i < n:
|
|
j = integerify(buffer) & n_mask
|
|
result = tuple(a ^ b for a, b in izip(buffer, get_v_elem(j)))
|
|
bmix(result, buffer)
|
|
i += 1
|
|
|
|
# # NOTE: we could easily support arbitrary values of ``n``, not just powers of 2,
|
|
# # but very few implementations have that ability, so not enabling it for now...
|
|
# if not n_is_log_2:
|
|
# while i < n:
|
|
# j = integerify(buffer) % n
|
|
# tmp = tuple(a^b for a,b in izip(buffer, get_v_elem(j)))
|
|
# bmix(tmp,buffer)
|
|
# i += 1
|
|
|
|
# repack tmp
|
|
return bmix_struct.pack(*buffer)
|
|
|
|
#=================================================================
|
|
# bmix() helper
|
|
#=================================================================
|
|
def bmix(self, source, target):
|
|
"""
|
|
block mixing function used by smix()
|
|
uses salsa20/8 core to mix block contents.
|
|
|
|
:arg source:
|
|
source to read from.
|
|
should be list of 32*r 4-byte integers
|
|
(2*r salsa20 blocks).
|
|
|
|
:arg target:
|
|
target to write to.
|
|
should be list with same size as source.
|
|
the existing value of this buffer is ignored.
|
|
|
|
.. warning::
|
|
|
|
this operates *in place* on target,
|
|
so source & target should NOT be same list.
|
|
|
|
.. note::
|
|
|
|
* time cost is ``O(r)`` -- loops 16*r times, salsa20() has ``O(1)`` cost.
|
|
|
|
* memory cost is ``O(1)`` -- salsa20() uses 16 x uint4,
|
|
all other operations done in-place.
|
|
"""
|
|
## assert source is not target
|
|
# Y[-1] = B[2r-1], Y[i] = hash( Y[i-1] xor B[i])
|
|
# B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */
|
|
half = self.bmix_half_len # 16*r out of 32*r - start of Y_1
|
|
tmp = source[-16:] # 'X' in scrypt source
|
|
siter = iter(source)
|
|
j = 0
|
|
while j < half:
|
|
jn = j+16
|
|
target[j:jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter))
|
|
target[half+j:half+jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter))
|
|
j = jn
|
|
|
|
def _bmix_1(self, source, target):
|
|
"""special bmix() method optimized for ``r=1`` case"""
|
|
B = source[16:]
|
|
target[:16] = tmp = salsa20(a ^ b for a, b in izip(B, iter(source)))
|
|
target[16:] = salsa20(a ^ b for a, b in izip(tmp, B))
|
|
|
|
#=================================================================
|
|
# eoc
|
|
#=================================================================
|
|
|
|
#==========================================================================
|
|
# eof
|
|
#==========================================================================
|