Source code for hkdfs.hkdfs

"""
HMAC-based key derivation function (HKDF) standalone implementation using pure
Python.
"""
from __future__ import annotations
from typing import Union, Optional
from collections.abc import Callable
import doctest
import hashlib
import hmac

def _hkdf_extract(
        input_key_material: Union[bytes, bytearray],
        salt: Optional[Union[bytes, bytearray]] = None,
        hash: Callable[ # pylint: disable=redefined-builtin
            [Union[bytes, bytearray]],
            hashlib._hashlib.HASH
        ] = hashlib.sha256
    ) -> bytes:
    """
    Extract a pseudorandom key (PRK) using HMAC with the given input key
    material and salt. If the salt is empty, a zero-filled byte string (of
    the same length as the hash function's digest) is used.

    :param input_key_material: Initial key material.
    :param salt: Additional salt to incorporate during extraction.
    :param hash: Hash function to use when performing the extraction.
    """
    return hmac.new(
        salt or bytes([0] * hash().digest_size),
        input_key_material,
        hash
    ).digest()

def _hkdf_expand(
        length: int,
        pseudorandom_key: Union[bytes, bytearray],
        info: Optional[Union[bytes, bytearray]] = None,
        hash: Callable[ # pylint: disable=redefined-builtin
            [Union[bytes, bytearray]],
            hashlib._hashlib.HASH
        ] = hashlib.sha256
    ) -> bytes:
    """
    Expand the supplied pseudorandom key into output key material of the
    specified length using HMAC-based expansion.

    :param length: Target length of output key.
    :param pseudorandom_key: Pseudorandom key to expand.
    :param info: Additional binary data to incorporate during expansion.
    :param hash: Hash function to use when performing the extraction.
    """
    length_maximum = 255 * hash().digest_size
    if length > length_maximum:
        raise ValueError(
            'maximum length supported by supplied hash function is ' +
            str(length_maximum)
        )

    info = info or bytes()
    digest = bytes()
    output_key_material = bytes()
    i = 0
    while len(output_key_material) < length:
        i += 1
        digest = hmac.new(
            pseudorandom_key,
            digest + info + bytes([i]),
            hash
        ).digest()
        output_key_material += digest

    return output_key_material[:length]

[docs] def hkdfs( length: int, key: Union[bytes, bytearray], salt: Optional[Union[bytes, bytearray]] = None, info: Optional[Union[bytes, bytearray]] = None, hash: Callable[ # pylint: disable=redefined-builtin [Union[bytes, bytearray]], hashlib._hashlib.HASH ] = hashlib.sha256 ) -> bytes: """ Extract a pseudorandom key having ``length`` bytes from ``key`` (and optionally also from ``salt`` and ``info``). :param length: Target length of output key. :param key: Pseudorandom key to expand. :param salt: Additional salt to incorporate during extraction. :param info: Additional binary data to incorporate during expansion. >>> hkdfs(1024, bytes([123]), hash=hashlib.sha512).hex() '4936e6f3ad5e6cab0efd42e2f216d34b977...1bc59c8e55db51d239808e8465a3cb91d11' >>> hkdfs( ... length=1024, ... key=bytes([1]), ... salt=bytes([2]), ... info=bytes([3]), ... hash=hashlib.sha512 ... ).hex()[:73] '1277a50c8cd05020dc073bd129cd84214270a0468e936c496fafee48c10a613a1a3b10fd2' Note that the maximum supported target length is determined by the length of the output of the supplied hash function. >>> hkdfs(255 * 32 + 1, bytes([123]), hash=hashlib.sha256) Traceback (most recent call last): ... ValueError: maximum length supported by supplied hash function is 8160 >>> len(hkdfs(255 * 32 + 1, bytes([123]), hash=hashlib.sha512)) 8161 >>> hkdfs(255 * 64 + 1, bytes([123]), hash=hashlib.sha512) Traceback (most recent call last): ... ValueError: maximum length supported by supplied hash function is 16320 The below tests correspond to the test cases found in Appendix A of `RFC 5869 <https://www.rfc-editor.org/rfc/rfc5869>`__. >>> hkdfs( # Test Case 1: Basic test case with SHA-256 ... length=42, ... key=bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), ... salt=bytes.fromhex('000102030405060708090a0b0c'), ... info=bytes.fromhex('f0f1f2f3f4f5f6f7f8f9'), ... hash=hashlib.sha256 ... ).hex() == ( ... '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf' + ... '1a5a4c5db02d56ecc4c5bf34007208d5b887185865' ... ) True >>> hkdfs( # Test Case 2: Test with SHA-256 and longer inputs/outputs ... length=82, ... key=bytes.fromhex( ... '000102030405060708090a0b0c0d0e0f' + ... '101112131415161718191a1b1c1d1e1f' + ... '202122232425262728292a2b2c2d2e2f' + ... '303132333435363738393a3b3c3d3e3f' + ... '404142434445464748494a4b4c4d4e4f' ... ), ... salt=bytes.fromhex( ... '606162636465666768696a6b6c6d6e6f' + ... '707172737475767778797a7b7c7d7e7f' + ... '808182838485868788898a8b8c8d8e8f' + ... '909192939495969798999a9b9c9d9e9f' + ... 'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf' ... ), ... info=bytes.fromhex( ... 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' + ... 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf' + ... 'd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' + ... 'e0e1e2e3e4e5e6e7e8e9eaebecedeeef' + ... 'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff' ... ), ... hash=hashlib.sha256 ... ).hex() == ( ... 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97' + ... 'c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db' + ... '71cc30c58179ec3e87c14c01d5c1f3434f1d87' ... ) True >>> hkdfs( # Test Case 3: Test with SHA-256 and zero-length salt/info ... length=42, ... key=bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), ... salt=bytes(0), ... info=bytes(0), ... hash=hashlib.sha256 ... ).hex() == ( ... '8da4e775a563c18f715f802a063c5a31b8a11f5c5e' + ... 'e1879ec3454e5f3c738d2d9d201395faa4b61a96c8' ... ) True >>> hkdfs( # Test Case 4: Basic test case with SHA-1 ... length=42, ... key=bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b'), ... salt=bytes.fromhex('000102030405060708090a0b0c'), ... info=bytes.fromhex('f0f1f2f3f4f5f6f7f8f9'), ... hash=hashlib.sha1 ... ).hex() == ( ... '085a01ea1b10f36933068b56efa5ad81a4f14b822f' + ... '5b091568a9cdd4f155fda2c22e422478d305f3f896' ... ) True >>> hkdfs( # Test Case 5: Test with SHA-1 and longer inputs/outputs ... length=82, ... key=bytes.fromhex( ... '000102030405060708090a0b0c0d0e0f' + ... '101112131415161718191a1b1c1d1e1f' + ... '202122232425262728292a2b2c2d2e2f' + ... '303132333435363738393a3b3c3d3e3f' + ... '404142434445464748494a4b4c4d4e4f' ... ), ... salt=bytes.fromhex( ... '606162636465666768696a6b6c6d6e6f' + ... '707172737475767778797a7b7c7d7e7f' + ... '808182838485868788898a8b8c8d8e8f' + ... '909192939495969798999a9b9c9d9e9f' + ... 'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf' ... ), ... info=bytes.fromhex( ... 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' + ... 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf' + ... 'd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' + ... 'e0e1e2e3e4e5e6e7e8e9eaebecedeeef' + ... 'f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff' ... ), ... hash=hashlib.sha1 ... ).hex() == ( ... '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ff' + ... 'e8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c' + ... '5e927336d0441f4c4300e2cff0d0900b52d3b4' ... ) True >>> hkdfs( # Test Case 6: Test with SHA-1 and zero-length salt/info ... length=42, ... key=bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), ... salt=bytes(0), ... info=bytes(0), ... hash=hashlib.sha1 ... ).hex() == ( ... '0ac1af7002b3d761d1e55298da9d0506b9ae520572' + ... '20a306e07b6b87e8df21d0ea00033de03984d34918' ... ) True >>> hkdfs( # Test Case 7: Test with SHA-1, no salt, and zero-length info ... length=42, ... key=bytes.fromhex('0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c'), ... info=bytes(0), ... hash=hashlib.sha1 ... ).hex() == ( ... '2c91117204d745f3500d636a62f64f0ab3bae548aa' + ... '53d423b0d1f27ebba6f5e5673a081d70cce7acfc48' ... ) True This function performs type and range checking, raising an exception when invoked with invalid arguments. >>> hkdfs('abc', bytes([1])) Traceback (most recent call last): ... TypeError: length must be an integer >>> hkdfs(-1, bytes([1])) Traceback (most recent call last): ... ValueError: length must be a nonnegative integer >>> hkdfs(1024, 'abc') Traceback (most recent call last): ... TypeError: key must be a bytes-like object >>> hkdfs(1024, bytes([1]), 'abc') Traceback (most recent call last): ... TypeError: salt must be a bytes-like object >>> hkdfs(1024, bytes([1]), bytes([2]), 'abc') Traceback (most recent call last): ... TypeError: info must be a bytes-like object The final optional argument ``hash`` is normally expected be a valid hash function from the built-in :obj:`hashlib` module (for example, the function :obj:`hashlib.sha512`). However, any object that matches the interface of the example class ``digestmod`` below can be supplied. >>> class digestmod: ... digest_size: int = 64 ... block_size: int = 64 ... ... def update(d: digestmod, b: bytes): ... pass ... ... def copy(self: digestmod) -> digestmod: ... return digestmod() ... ... def digest(self: digestmod) -> bytes: ... return bytes(64) ... >>> hkdfs(1024, bytes([123]), hash=digestmod) == bytes(1024) True No checks are performed to confirm that the supplied value for ``hash`` conforms to the above interface. A deviation from this interface may cause an exception to be raised by an underlying internal or built-in function. This exception will either be a :obj:`TypeError` because the value is not callable, an :obj:`AttributeError` because an expected attribute is missing, or another error because the attributes do not behave as would be expected for a built-in hash function. >>> hkdfs(1024, bytes([123]), hash=123) Traceback (most recent call last): ... TypeError: 'int' object is not callable >>> class digestmod: ... digest_size: int = 64 ... block_size: int = 64 ... >>> hkdfs(1024, bytes([123]), hash=digestmod) Traceback (most recent call last): ... AttributeError: 'digestmod' object has no attribute 'update' Consult the documentation for :obj:`hashlib.new` for more information. """ if not isinstance(length, int): raise TypeError('length must be an integer') if not isinstance(key, (bytes, bytearray)): raise TypeError('key must be a bytes-like object') if salt is not None and not isinstance(salt, (bytes, bytearray)): raise TypeError('salt must be a bytes-like object') if info is not None and not isinstance(info, (bytes, bytearray)): raise TypeError('info must be a bytes-like object') if length < 0: raise ValueError('length must be a nonnegative integer') return _hkdf_expand(length, _hkdf_extract(key, salt, hash), info, hash)
if __name__ == '__main__': doctest.testmod() # pragma: no cover