262 lines
7.2 KiB
Python
262 lines
7.2 KiB
Python
# coding: utf-8
|
|
from __future__ import unicode_literals, division, absolute_import, print_function
|
|
|
|
import sys
|
|
import hashlib
|
|
from datetime import datetime
|
|
|
|
from . import backend
|
|
from .util import rand_bytes
|
|
from ._types import type_name, byte_cls, int_types
|
|
from ._errors import pretty_message
|
|
from ._ffi import new, deref
|
|
|
|
|
|
_backend = backend()
|
|
|
|
|
|
if _backend == 'mac':
|
|
from ._mac.util import pbkdf2, pkcs12_kdf
|
|
elif _backend == 'win' or _backend == 'winlegacy':
|
|
from ._win.util import pbkdf2, pkcs12_kdf
|
|
from ._win._kernel32 import kernel32, handle_error
|
|
else:
|
|
from ._openssl.util import pbkdf2, pkcs12_kdf
|
|
|
|
|
|
__all__ = [
|
|
'pbkdf1',
|
|
'pbkdf2',
|
|
'pbkdf2_iteration_calculator',
|
|
'pkcs12_kdf',
|
|
]
|
|
|
|
|
|
if sys.platform == 'win32':
|
|
def _get_start():
|
|
number = new(kernel32, 'LARGE_INTEGER *')
|
|
res = kernel32.QueryPerformanceCounter(number)
|
|
handle_error(res)
|
|
return deref(number)
|
|
|
|
def _get_elapsed(start):
|
|
length = _get_start() - start
|
|
return int(length / 1000.0)
|
|
|
|
else:
|
|
def _get_start():
|
|
return datetime.now()
|
|
|
|
def _get_elapsed(start):
|
|
length = datetime.now() - start
|
|
seconds = length.seconds + (length.days * 24 * 3600)
|
|
milliseconds = (length.microseconds / 10 ** 3)
|
|
return int(milliseconds + (seconds * 10 ** 3))
|
|
|
|
|
|
def pbkdf2_iteration_calculator(hash_algorithm, key_length, target_ms=100, quiet=False):
|
|
"""
|
|
Runs pbkdf2() twice to determine the approximate number of iterations to
|
|
use to hit a desired time per run. Use this on a production machine to
|
|
dynamically adjust the number of iterations as high as you can.
|
|
|
|
:param hash_algorithm:
|
|
The string name of the hash algorithm to use: "md5", "sha1", "sha224",
|
|
"sha256", "sha384", "sha512"
|
|
|
|
:param key_length:
|
|
The length of the desired key in bytes
|
|
|
|
:param target_ms:
|
|
The number of milliseconds the derivation should take
|
|
|
|
:param quiet:
|
|
If no output should be printed as attempts are made
|
|
|
|
:return:
|
|
An integer number of iterations of PBKDF2 using the specified hash
|
|
that will take at least target_ms
|
|
"""
|
|
|
|
if hash_algorithm not in set(['sha1', 'sha224', 'sha256', 'sha384', 'sha512']):
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
hash_algorithm must be one of "sha1", "sha224", "sha256", "sha384",
|
|
"sha512", not %s
|
|
''',
|
|
repr(hash_algorithm)
|
|
))
|
|
|
|
if not isinstance(key_length, int_types):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
key_length must be an integer, not %s
|
|
''',
|
|
type_name(key_length)
|
|
))
|
|
|
|
if key_length < 1:
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
key_length must be greater than 0 - is %s
|
|
''',
|
|
repr(key_length)
|
|
))
|
|
|
|
if not isinstance(target_ms, int_types):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
target_ms must be an integer, not %s
|
|
''',
|
|
type_name(target_ms)
|
|
))
|
|
|
|
if target_ms < 1:
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
target_ms must be greater than 0 - is %s
|
|
''',
|
|
repr(target_ms)
|
|
))
|
|
|
|
if pbkdf2.pure_python:
|
|
raise OSError(pretty_message(
|
|
'''
|
|
Only a very slow, pure-python version of PBKDF2 is available,
|
|
making this function useless
|
|
'''
|
|
))
|
|
|
|
iterations = 10000
|
|
password = 'this is a test'.encode('utf-8')
|
|
salt = rand_bytes(key_length)
|
|
|
|
def _measure():
|
|
start = _get_start()
|
|
pbkdf2(hash_algorithm, password, salt, iterations, key_length)
|
|
observed_ms = _get_elapsed(start)
|
|
if not quiet:
|
|
print('%s iterations in %sms' % (iterations, observed_ms))
|
|
return 1.0 / target_ms * observed_ms
|
|
|
|
# Measure the initial guess, then estimate how many iterations it would
|
|
# take to reach 1/2 of the target ms and try it to get a good final number
|
|
fraction = _measure()
|
|
iterations = int(iterations / fraction / 2.0)
|
|
|
|
fraction = _measure()
|
|
iterations = iterations / fraction
|
|
|
|
# < 20,000 round to 1000
|
|
# 20,000-100,000 round to 5,000
|
|
# > 100,000 round to 10,000
|
|
round_factor = -3 if iterations < 100000 else -4
|
|
result = int(round(iterations, round_factor))
|
|
if result > 20000:
|
|
result = (result // 5000) * 5000
|
|
return result
|
|
|
|
|
|
def pbkdf1(hash_algorithm, password, salt, iterations, key_length):
|
|
"""
|
|
An implementation of PBKDF1 - should only be used for interop with legacy
|
|
systems, not new architectures
|
|
|
|
:param hash_algorithm:
|
|
The string name of the hash algorithm to use: "md2", "md5", "sha1"
|
|
|
|
:param password:
|
|
A byte string of the password to use an input to the KDF
|
|
|
|
:param salt:
|
|
A cryptographic random byte string
|
|
|
|
:param iterations:
|
|
The numbers of iterations to use when deriving the key
|
|
|
|
:param key_length:
|
|
The length of the desired key in bytes
|
|
|
|
:return:
|
|
The derived key as a byte string
|
|
"""
|
|
|
|
if not isinstance(password, byte_cls):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
password must be a byte string, not %s
|
|
''',
|
|
(type_name(password))
|
|
))
|
|
|
|
if not isinstance(salt, byte_cls):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
salt must be a byte string, not %s
|
|
''',
|
|
(type_name(salt))
|
|
))
|
|
|
|
if not isinstance(iterations, int_types):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
iterations must be an integer, not %s
|
|
''',
|
|
(type_name(iterations))
|
|
))
|
|
|
|
if iterations < 1:
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
iterations must be greater than 0 - is %s
|
|
''',
|
|
repr(iterations)
|
|
))
|
|
|
|
if not isinstance(key_length, int_types):
|
|
raise TypeError(pretty_message(
|
|
'''
|
|
key_length must be an integer, not %s
|
|
''',
|
|
(type_name(key_length))
|
|
))
|
|
|
|
if key_length < 1:
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
key_length must be greater than 0 - is %s
|
|
''',
|
|
repr(key_length)
|
|
))
|
|
|
|
if hash_algorithm not in set(['md2', 'md5', 'sha1']):
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
hash_algorithm must be one of "md2", "md5", "sha1", not %s
|
|
''',
|
|
repr(hash_algorithm)
|
|
))
|
|
|
|
if key_length > 16 and hash_algorithm in set(['md2', 'md5']):
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
key_length can not be longer than 16 for %s - is %s
|
|
''',
|
|
(hash_algorithm, repr(key_length))
|
|
))
|
|
|
|
if key_length > 20 and hash_algorithm == 'sha1':
|
|
raise ValueError(pretty_message(
|
|
'''
|
|
key_length can not be longer than 20 for sha1 - is %s
|
|
''',
|
|
repr(key_length)
|
|
))
|
|
|
|
algo = getattr(hashlib, hash_algorithm)
|
|
output = algo(password + salt).digest()
|
|
for _ in range(2, iterations + 1):
|
|
output = algo(output).digest()
|
|
|
|
return output[:key_length]
|