# py-multihash: Python implementation of the multihash specification
"""Enumeration of standard multihash functions, and function registry.
This module provides:
- Func: IntEnum of supported hash functions
- FuncReg: Registry for managing hash function implementations
- IdentityHash: hashlib-compatible identity hash
- ShakeHash: Wrapper for variable-length SHAKE hashes
The FuncReg class maintains a registry of hash functions that can be:
- Retrieved by code, name, or hashlib object
- Extended with custom app-specific functions (codes 0x01-0x0F)
- Used to create hashlib-compatible hash objects
Standard functions are pre-registered. App-specific functions can be
registered/unregistered at runtime.
"""
import hashlib
from collections import namedtuple
from enum import IntEnum
from numbers import Integral
from typing import ClassVar
import blake3
import mmh3
from .constants import HASH_CODES
def _is_app_specific_func(code: int) -> bool:
"""Is the given hash function integer `code` application-specific?"""
return isinstance(code, Integral) and (0x01 <= code <= 0x0F)
[docs]
class Func(IntEnum):
"""An enumeration of hash functions supported by multihash.
The name of each member has its hyphens replaced by underscores.
The value of each member corresponds to its integer code.
>>> Func.sha2_512.value == 0x13
True
"""
# Blake2 variants (sorted alphabetically)
blake2b_8 = HASH_CODES["blake2b-8"] # 0xB201
blake2b_16 = HASH_CODES["blake2b-16"] # 0xB202
blake2b_24 = HASH_CODES["blake2b-24"] # 0xB203
blake2b_32 = HASH_CODES["blake2b-32"] # 0xB204
blake2b_40 = HASH_CODES["blake2b-40"] # 0xB205
blake2b_48 = HASH_CODES["blake2b-48"] # 0xB206
blake2b_56 = HASH_CODES["blake2b-56"] # 0xB207
blake2b_64 = HASH_CODES["blake2b-64"] # 0xB208
blake2b_72 = HASH_CODES["blake2b-72"] # 0xB209
blake2b_80 = HASH_CODES["blake2b-80"] # 0xB20A
blake2b_88 = HASH_CODES["blake2b-88"] # 0xB20B
blake2b_96 = HASH_CODES["blake2b-96"] # 0xB20C
blake2b_104 = HASH_CODES["blake2b-104"] # 0xB20D
blake2b_112 = HASH_CODES["blake2b-112"] # 0xB20E
blake2b_120 = HASH_CODES["blake2b-120"] # 0xB20F
blake2b_128 = HASH_CODES["blake2b-128"] # 0xB210
blake2b_136 = HASH_CODES["blake2b-136"] # 0xB211
blake2b_144 = HASH_CODES["blake2b-144"] # 0xB212
blake2b_152 = HASH_CODES["blake2b-152"] # 0xB213
blake2b_160 = HASH_CODES["blake2b-160"] # 0xB214
blake2b_168 = HASH_CODES["blake2b-168"] # 0xB215
blake2b_176 = HASH_CODES["blake2b-176"] # 0xB216
blake2b_184 = HASH_CODES["blake2b-184"] # 0xB217
blake2b_192 = HASH_CODES["blake2b-192"] # 0xB218
blake2b_200 = HASH_CODES["blake2b-200"] # 0xB219
blake2b_208 = HASH_CODES["blake2b-208"] # 0xB21A
blake2b_216 = HASH_CODES["blake2b-216"] # 0xB21B
blake2b_224 = HASH_CODES["blake2b-224"] # 0xB21C
blake2b_232 = HASH_CODES["blake2b-232"] # 0xB21D
blake2b_240 = HASH_CODES["blake2b-240"] # 0xB21E
blake2b_248 = HASH_CODES["blake2b-248"] # 0xB21F
blake2b_256 = HASH_CODES["blake2b-256"] # 0xB220
blake2b_264 = HASH_CODES["blake2b-264"] # 0xB221
blake2b_272 = HASH_CODES["blake2b-272"] # 0xB222
blake2b_280 = HASH_CODES["blake2b-280"] # 0xB223
blake2b_288 = HASH_CODES["blake2b-288"] # 0xB224
blake2b_296 = HASH_CODES["blake2b-296"] # 0xB225
blake2b_304 = HASH_CODES["blake2b-304"] # 0xB226
blake2b_312 = HASH_CODES["blake2b-312"] # 0xB227
blake2b_320 = HASH_CODES["blake2b-320"] # 0xB228
blake2b_328 = HASH_CODES["blake2b-328"] # 0xB229
blake2b_336 = HASH_CODES["blake2b-336"] # 0xB22A
blake2b_344 = HASH_CODES["blake2b-344"] # 0xB22B
blake2b_352 = HASH_CODES["blake2b-352"] # 0xB22C
blake2b_360 = HASH_CODES["blake2b-360"] # 0xB22D
blake2b_368 = HASH_CODES["blake2b-368"] # 0xB22E
blake2b_376 = HASH_CODES["blake2b-376"] # 0xB22F
blake2b_384 = HASH_CODES["blake2b-384"] # 0xB230
blake2b_392 = HASH_CODES["blake2b-392"] # 0xB231
blake2b_400 = HASH_CODES["blake2b-400"] # 0xB232
blake2b_408 = HASH_CODES["blake2b-408"] # 0xB233
blake2b_416 = HASH_CODES["blake2b-416"] # 0xB234
blake2b_424 = HASH_CODES["blake2b-424"] # 0xB235
blake2b_432 = HASH_CODES["blake2b-432"] # 0xB236
blake2b_440 = HASH_CODES["blake2b-440"] # 0xB237
blake2b_448 = HASH_CODES["blake2b-448"] # 0xB238
blake2b_456 = HASH_CODES["blake2b-456"] # 0xB239
blake2b_464 = HASH_CODES["blake2b-464"] # 0xB23A
blake2b_472 = HASH_CODES["blake2b-472"] # 0xB23B
blake2b_480 = HASH_CODES["blake2b-480"] # 0xB23C
blake2b_488 = HASH_CODES["blake2b-488"] # 0xB23D
blake2b_496 = HASH_CODES["blake2b-496"] # 0xB23E
blake2b_504 = HASH_CODES["blake2b-504"] # 0xB23F
blake2b_512 = HASH_CODES["blake2b-512"] # 0xB240
blake2s_8 = HASH_CODES["blake2s-8"] # 0xB241
blake2s_16 = HASH_CODES["blake2s-16"] # 0xB242
blake2s_24 = HASH_CODES["blake2s-24"] # 0xB243
blake2s_32 = HASH_CODES["blake2s-32"] # 0xB244
blake2s_40 = HASH_CODES["blake2s-40"] # 0xB245
blake2s_48 = HASH_CODES["blake2s-48"] # 0xB246
blake2s_56 = HASH_CODES["blake2s-56"] # 0xB247
blake2s_64 = HASH_CODES["blake2s-64"] # 0xB248
blake2s_72 = HASH_CODES["blake2s-72"] # 0xB249
blake2s_80 = HASH_CODES["blake2s-80"] # 0xB24A
blake2s_88 = HASH_CODES["blake2s-88"] # 0xB24B
blake2s_96 = HASH_CODES["blake2s-96"] # 0xB24C
blake2s_104 = HASH_CODES["blake2s-104"] # 0xB24D
blake2s_112 = HASH_CODES["blake2s-112"] # 0xB24E
blake2s_120 = HASH_CODES["blake2s-120"] # 0xB24F
blake2s_128 = HASH_CODES["blake2s-128"] # 0xB250
blake2s_136 = HASH_CODES["blake2s-136"] # 0xB251
blake2s_144 = HASH_CODES["blake2s-144"] # 0xB252
blake2s_152 = HASH_CODES["blake2s-152"] # 0xB253
blake2s_160 = HASH_CODES["blake2s-160"] # 0xB254
blake2s_168 = HASH_CODES["blake2s-168"] # 0xB255
blake2s_176 = HASH_CODES["blake2s-176"] # 0xB256
blake2s_184 = HASH_CODES["blake2s-184"] # 0xB257
blake2s_192 = HASH_CODES["blake2s-192"] # 0xB258
blake2s_200 = HASH_CODES["blake2s-200"] # 0xB259
blake2s_208 = HASH_CODES["blake2s-208"] # 0xB25A
blake2s_216 = HASH_CODES["blake2s-216"] # 0xB25B
blake2s_224 = HASH_CODES["blake2s-224"] # 0xB25C
blake2s_232 = HASH_CODES["blake2s-232"] # 0xB25D
blake2s_240 = HASH_CODES["blake2s-240"] # 0xB25E
blake2s_248 = HASH_CODES["blake2s-248"] # 0xB25F
blake2s_256 = HASH_CODES["blake2s-256"] # 0xB260
blake3 = HASH_CODES["blake3"] # 0x1E
dbl_sha2_256 = HASH_CODES["dbl-sha2-256"] # 0x56
identity = HASH_CODES["id"] # 0x00
keccak_224 = HASH_CODES["keccak-224"] # 0x1A
keccak_256 = HASH_CODES["keccak-256"] # 0x1B
keccak_384 = HASH_CODES["keccak-384"] # 0x1C
keccak_512 = HASH_CODES["keccak-512"] # 0x1D
md4 = HASH_CODES["md4"] # 0xD4
md5 = HASH_CODES["md5"] # 0xD5
murmur3_128 = HASH_CODES["murmur3-128"] # 0x22
murmur3_32 = HASH_CODES["murmur3-32"] # 0x23
ripemd_128 = HASH_CODES["ripemd-128"] # 0x1052
ripemd_160 = HASH_CODES["ripemd-160"] # 0x1053
ripemd_256 = HASH_CODES["ripemd-256"] # 0x1054
ripemd_320 = HASH_CODES["ripemd-320"] # 0x1055
sha1 = HASH_CODES["sha1"] # 0x11
sha2_224 = HASH_CODES["sha2-224"] # 0x1013
sha2_256 = HASH_CODES["sha2-256"] # 0x12
sha2_256_trunc254_padded = HASH_CODES["sha2-256-trunc254-padded"] # 0x1012
sha2_384 = HASH_CODES["sha2-384"] # 0x20
sha2_512 = HASH_CODES["sha2-512"] # 0x13
sha2_512_224 = HASH_CODES["sha2-512-224"] # 0x1014
sha2_512_256 = HASH_CODES["sha2-512-256"] # 0x1015
sha3_224 = HASH_CODES["sha3-224"] # 0x17
sha3_256 = HASH_CODES["sha3-256"] # 0x16
sha3_384 = HASH_CODES["sha3-384"] # 0x15
sha3_512 = HASH_CODES["sha3-512"] # 0x14
shake_128 = HASH_CODES["shake-128"] # 0x18
shake_256 = HASH_CODES["shake-256"] # 0x19
[docs]
class IdentityHash:
"""hashlib-compatible algorithm where the input is the digest."""
name: str = "identity"
def __init__(self) -> None:
self._data = b""
@property
def digest_size(self) -> int:
return len(self._data)
@property
def block_size(self) -> int:
return 1
[docs]
def update(self, data: bytes) -> None:
self._data += data
[docs]
def digest(self) -> bytes:
return self._data
[docs]
def hexdigest(self) -> str:
return self._data.hex()
[docs]
def copy(self) -> "IdentityHash":
c = IdentityHash()
c._data = self._data
return c
class _FuncRegMeta(type):
_func_hash: dict
def __contains__(cls, func) -> bool:
"""Return whether `func` is a registered function."""
return func in cls._func_hash
def __iter__(cls):
"""Iterate over registered functions."""
return iter(cls._func_hash)
[docs]
class ShakeHash:
"""Wrapper for SHAKE variable-length hash functions."""
def __init__(self, shake_func, length: int):
"""Initialize SHAKE hash with specified output length.
Args:
shake_func: hashlib.shake_128 or hashlib.shake_256
length: Output digest length in bytes
"""
self._shake_func = shake_func
self._hasher = shake_func()
self._length = length
self.name = self._hasher.name
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._hasher.update(data)
[docs]
def digest(self) -> bytes:
"""Return digest of specified length."""
return self._hasher.digest(self._length)
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self.digest().hex()
[docs]
def copy(self) -> "ShakeHash":
"""Create a copy of the hash state."""
c = ShakeHash(self._shake_func, self._length)
c._hasher = self._hasher.copy()
return c
[docs]
class Blake3Hash:
"""hashlib-compatible wrapper for Blake3 using official blake3 library.
BLAKE3 is a cryptographic hash function that is much faster than MD5, SHA-1, SHA-2,
and SHA-3, yet is just as secure as the latest standard SHA-3.
Example:
>>> from multihash import digest
>>> mh = digest(b"hello world", "blake3")
>>> mh.digest.hex() # doctest: +ELLIPSIS
'...'
>>> # Or use the hash class directly:
>>> h = Blake3Hash()
>>> h.update(b"hello ")
>>> h.update(b"world")
>>> h.hexdigest() # doctest: +ELLIPSIS
'...'
"""
name: str = "blake3"
digest_size: int = 32
block_size: int = 64
def __init__(self) -> None:
self._hasher = blake3.blake3()
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._hasher.update(data)
[docs]
def digest(self) -> bytes:
"""Return digest."""
return self._hasher.digest()
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self._hasher.hexdigest()
[docs]
def copy(self) -> "Blake3Hash":
"""Create a copy of the hash state."""
c = Blake3Hash()
c._hasher = self._hasher.copy()
return c
[docs]
class Murmur3_128Hash:
"""hashlib-compatible wrapper for MurmurHash3 128-bit using official mmh3 library.
MurmurHash3 is a fast, non-cryptographic hash function suitable for hash-based lookups.
Note: Not suitable for cryptographic purposes.
Example:
>>> from multihash import digest
>>> mh = digest(b"hello world", "murmur3-128")
>>> mh.digest.hex() # doctest: +ELLIPSIS
'...'
>>> # Or use the hash class directly with a custom seed:
>>> h = Murmur3_128Hash(seed=42)
>>> h.update(b"data")
>>> h.hexdigest() # doctest: +ELLIPSIS
'...'
"""
name: str = "murmur3-128"
digest_size: int = 16
block_size: int = 1
def __init__(self, seed: int = 0) -> None:
self._data = b""
self._seed = seed
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._data += data
[docs]
def digest(self) -> bytes:
"""Return 128-bit digest."""
# mmh3.hash128 returns a 128-bit integer
hash_value = mmh3.hash128(self._data, seed=self._seed, signed=False)
return hash_value.to_bytes(16, byteorder="little")
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self.digest().hex()
[docs]
def copy(self) -> "Murmur3_128Hash":
"""Create a copy of the hash state."""
c = Murmur3_128Hash(seed=self._seed)
c._data = self._data
return c
[docs]
class Murmur3_32Hash:
"""hashlib-compatible wrapper for MurmurHash3 32-bit using official mmh3 library.
MurmurHash3 32-bit variant is a fast, non-cryptographic hash function.
Note: Not suitable for cryptographic purposes.
Example:
>>> from multihash import digest
>>> mh = digest(b"hello world", "murmur3-32")
>>> mh.digest.hex() # doctest: +ELLIPSIS
'...'
>>> # Or use the hash class directly with a custom seed:
>>> h = Murmur3_32Hash(seed=0)
>>> h.update(b"data")
>>> h.hexdigest() # doctest: +ELLIPSIS
'...'
"""
name: str = "murmur3-32"
digest_size: int = 4
block_size: int = 1
def __init__(self, seed: int = 0) -> None:
self._data = b""
self._seed = seed
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._data += data
[docs]
def digest(self) -> bytes:
"""Return 32-bit digest."""
# mmh3.hash returns a 32-bit integer
hash_value = mmh3.hash(self._data, seed=self._seed, signed=False)
return hash_value.to_bytes(4, byteorder="little")
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self.digest().hex()
[docs]
def copy(self) -> "Murmur3_32Hash":
"""Create a copy of the hash state."""
c = Murmur3_32Hash(seed=self._seed)
c._data = self._data
return c
[docs]
class DoubleSHA256Hash:
"""hashlib-compatible wrapper for double SHA2-256 (used in Bitcoin).
Applies SHA-256 twice: SHA-256(SHA-256(data)). This is commonly used in Bitcoin
and other cryptocurrencies for additional security.
Example:
>>> from multihash import digest
>>> mh = digest(b"hello world", "dbl-sha2-256")
>>> mh.digest.hex() # doctest: +ELLIPSIS
'...'
>>> # Or use the hash class directly:
>>> h = DoubleSHA256Hash()
>>> h.update(b"data")
>>> h.hexdigest() # doctest: +ELLIPSIS
'...'
"""
name: str = "dbl-sha2-256"
digest_size: int = 32
block_size: int = 64
def __init__(self) -> None:
self._hasher = hashlib.sha256()
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._hasher.update(data)
[docs]
def digest(self) -> bytes:
"""Return digest (double SHA-256)."""
first_hash = self._hasher.digest()
return hashlib.sha256(first_hash).digest()
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self.digest().hex()
[docs]
def copy(self) -> "DoubleSHA256Hash":
"""Create a copy of the hash state."""
c = DoubleSHA256Hash()
c._hasher = self._hasher.copy()
return c
[docs]
class SHA2_256_Trunc254_Padded_Hash:
"""hashlib-compatible wrapper for SHA2-256 truncated to 254 bits and padded."""
name: str = "sha2-256-trunc254-padded"
digest_size: int = 31 # 254 bits = 31.75 bytes, but we use 31 bytes
block_size: int = 64
def __init__(self) -> None:
self._hasher = hashlib.sha256()
[docs]
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._hasher.update(data)
[docs]
def digest(self) -> bytes:
"""Return digest (SHA-256 truncated to 254 bits = 31 bytes)."""
full_hash = self._hasher.digest()
# Truncate to 254 bits (31 bytes) by taking first 31 bytes
return full_hash[:31]
[docs]
def hexdigest(self) -> str:
"""Return hex digest."""
return self.digest().hex()
[docs]
def copy(self) -> "SHA2_256_Trunc254_Padded_Hash":
"""Create a copy of the hash state."""
c = SHA2_256_Trunc254_Padded_Hash()
c._hasher = self._hasher.copy()
return c
def _create_blake2_variant(variant: str, digest_bytes: int):
"""Factory function to create Blake2 variant classes.
Args:
variant: Either 'blake2b' or 'blake2s'
digest_bytes: Number of bytes in the digest (1-64 for blake2b, 1-32 for blake2s)
Returns:
A hashlib-compatible hash class for the specified Blake2 variant
Example:
>>> from multihash import digest
>>> # Use BLAKE2b with 256-bit output
>>> mh = digest(b"hello", "blake2b-256")
>>>
>>> # Use BLAKE2s with 128-bit output
>>> mh = digest(b"hello", "blake2s-128")
>>>
>>> # All variants from 8 to 512 bits (blake2b) or 8 to 256 bits (blake2s) are supported
>>> mh = digest(b"data", "blake2b-384")
"""
class Blake2Variant:
name: str = f"{variant}-{digest_bytes * 8}"
digest_size: int = digest_bytes
block_size: int = 128 if variant == "blake2b" else 64
def __init__(self) -> None:
hash_func = getattr(hashlib, variant)
self._hasher = hash_func(digest_size=digest_bytes)
def update(self, data: bytes) -> None:
"""Update the hash with data."""
self._hasher.update(data)
def digest(self) -> bytes:
"""Return digest."""
return self._hasher.digest()
def hexdigest(self) -> str:
"""Return hex digest."""
return self._hasher.hexdigest()
def copy(self) -> "Blake2Variant":
"""Create a copy of the hash state."""
c = Blake2Variant()
c._hasher = self._hasher.copy()
return c
return Blake2Variant
[docs]
class FuncReg(metaclass=_FuncRegMeta):
"""Registry of supported hash functions.
The FuncReg class maintains a registry of hash functions that can be:
- Retrieved by code, name, or hashlib object
- Extended with custom app-specific functions (codes 0x01-0x0F)
- Used to create hashlib-compatible hash objects
Standard functions are pre-registered. App-specific functions can be
registered/unregistered at runtime.
Example:
Register an app-specific hash function:
>>> FuncReg.register(0x05, "my-custom-hash", "myhash", lambda: MyHash())
>>> func = FuncReg.get("my-custom-hash")
>>> hash_obj = FuncReg.hash_from_func(func)
Unregister an app-specific function:
>>> FuncReg.unregister(0x05)
"""
_hash = namedtuple("_hash", "name new")
# Class-level registry attributes
_func_from_name: ClassVar[dict] = {}
_func_from_hash: ClassVar[dict] = {}
_func_hash: ClassVar[dict] = {}
# Standard hash function data: (func, hashlib_name, constructor)
_std_func_data: ClassVar[list] = [
(Func.identity, "identity", IdentityHash),
(Func.sha1, "sha1", hashlib.sha1),
(Func.sha2_256, "sha256", hashlib.sha256),
(Func.sha2_512, "sha512", hashlib.sha512),
(Func.sha3_512, "sha3_512", hashlib.sha3_512),
(Func.sha3_384, "sha3_384", hashlib.sha3_384),
(Func.sha3_256, "sha3_256", hashlib.sha3_256),
(Func.sha3_224, "sha3_224", hashlib.sha3_224),
(Func.shake_128, "shake_128", None), # Variable length - use ShakeHash wrapper
(Func.shake_256, "shake_256", None), # Variable length - use ShakeHash wrapper
(Func.blake2b_256, "blake2b", lambda: hashlib.blake2b(digest_size=32)),
(Func.blake2b_512, "blake2b", lambda: hashlib.blake2b(digest_size=64)),
(Func.blake2s_256, "blake2s", lambda: hashlib.blake2s(digest_size=32)),
(Func.md5, "md5", hashlib.md5),
]
# Additional hash functions (conditionally added if available)
_optional_func_data: ClassVar[list] = [
# SHA2 variants (if available in hashlib)
(Func.sha2_224, "sha224", hashlib.sha224 if hasattr(hashlib, "sha224") else None),
(Func.sha2_384, "sha384", hashlib.sha384 if hasattr(hashlib, "sha384") else None),
# SHA2-512 truncated variants (Python 3.6+)
(Func.sha2_512_224, "sha512_224", getattr(hashlib, "sha512_224", None)),
(Func.sha2_512_256, "sha512_256", getattr(hashlib, "sha512_256", None)),
# Blake3 (using official blake3 library)
(Func.blake3, "blake3", Blake3Hash),
# Murmur3 variants (using official mmh3 library)
(Func.murmur3_128, "murmur3-128", Murmur3_128Hash),
(Func.murmur3_32, "murmur3-32", Murmur3_32Hash),
# Double SHA2-256 (always available, uses hashlib)
(Func.dbl_sha2_256, "dbl-sha2-256", DoubleSHA256Hash),
# SHA2-256 truncated and padded
(Func.sha2_256_trunc254_padded, "sha2-256-trunc254-padded", SHA2_256_Trunc254_Padded_Hash),
# Legacy hash functions - RIPEMD variants
(Func.ripemd_128, "ripemd128", getattr(hashlib, "ripemd128", None)),
(Func.ripemd_160, "ripemd160", getattr(hashlib, "ripemd160", None)),
(Func.ripemd_256, "ripemd256", getattr(hashlib, "ripemd256", None)),
(Func.ripemd_320, "ripemd320", getattr(hashlib, "ripemd320", None)),
# MD4 (legacy, weak)
(Func.md4, "md4", getattr(hashlib, "md4", None)),
]
# Blake2 variants - generated programmatically
@classmethod
def _generate_blake2_variants(cls):
"""Generate all Blake2b and Blake2s variant registrations."""
blake2_variants = []
# Blake2b variants (8 to 512 bits, in 8-bit increments)
# Skip 256 and 512 as they're already in std_func_data
for bits in range(8, 520, 8):
if bits in (256, 512): # Skip already defined variants
continue
digest_bytes = bits // 8
func_name = f"blake2b_{bits}"
if hasattr(Func, func_name):
func = getattr(Func, func_name)
hash_name = f"blake2b-{bits}"
hash_class = _create_blake2_variant("blake2b", digest_bytes)
blake2_variants.append((func, hash_name, hash_class))
# Blake2s variants (8 to 256 bits, in 8-bit increments)
# Skip 256 as it's already in std_func_data
for bits in range(8, 264, 8):
if bits == 256: # Skip already defined variant
continue
digest_bytes = bits // 8
func_name = f"blake2s_{bits}"
if hasattr(Func, func_name):
func = getattr(Func, func_name)
hash_name = f"blake2s-{bits}"
hash_class = _create_blake2_variant("blake2s", digest_bytes)
blake2_variants.append((func, hash_name, hash_class))
return blake2_variants
[docs]
@classmethod
def reset(cls) -> None:
"""Reset the registry to the standard multihash functions."""
cls._func_from_name = {}
cls._func_from_hash = {}
cls._func_hash = {}
for func, hash_name, hash_new in cls._std_func_data:
cls._do_register(func, func.name, hash_name, hash_new)
# Register optional functions if available
for func, hash_name, hash_new in cls._optional_func_data:
if hash_new is not None:
try:
# Test that the function is actually available by creating an instance.
# We don't use the result, just verify it can be instantiated.
# The unused variable is intentional - we only care about the side effect
# of successful instantiation, not the hash object itself.
_ = hash_new()
cls._do_register(func, func.name, hash_name, hash_new)
except (AttributeError, ValueError, TypeError):
# Function not available, skip
pass
# Register Blake2 variants
for func, hash_name, hash_class in cls._generate_blake2_variants():
try:
# Test instantiation
_ = hash_class()
cls._do_register(func, func.name, hash_name, hash_class)
except (AttributeError, ValueError, TypeError):
# Variant not available, skip
pass
[docs]
@classmethod
def get(cls, func_hint: Func | str | int) -> Func | int:
"""Return a registered hash function matching the given hint."""
if isinstance(func_hint, int):
try:
return Func(func_hint)
except ValueError:
pass
if isinstance(func_hint, str) and func_hint in cls._func_from_name:
return cls._func_from_name[func_hint]
if isinstance(func_hint, int) and func_hint in cls._func_hash:
return func_hint
raise KeyError("unknown hash function", func_hint)
@classmethod
def _do_register(cls, code: int, name: str, hash_name: str | None = None, hash_new=None) -> None:
"""Add hash function data to the registry without checks.
This method registers the function name in both hyphen and underscore
variants (e.g., "sha2-256" and "sha2_256") to provide flexibility
for users who may use either naming convention.
"""
cls._func_from_name[name.replace("-", "_")] = code
cls._func_from_name[name.replace("_", "-")] = code
if hash_name:
cls._func_from_hash[hash_name] = code
cls._func_hash[code] = cls._hash(hash_name, hash_new)
[docs]
@classmethod
def register(cls, code: int, name: str, hash_name: str | None = None, hash_new=None) -> None:
"""Add a function to the registry.
For standard functions already registered, this updates the hash_new
if provided. For app-specific functions (0x01-0x0f), replaces existing.
"""
# Check if this is a standard function
try:
Func(code)
is_std_func = True
except ValueError:
is_std_func = False
# For standard functions, just update hash_new if provided
if is_std_func and code in cls._func_hash:
if hash_new is not None:
old_hash = cls._func_hash[code]
cls._func_hash[code] = cls._hash(old_hash.name or hash_name, hash_new)
return
# Check for name conflicts
for mapping, nameinmap, errmsg in [
(cls._func_from_name, name, "function name already registered"),
(cls._func_from_hash, hash_name, "hashlib name already registered"),
]:
if nameinmap is None:
continue
existing = mapping.get(nameinmap, code)
if existing != code:
raise ValueError(errmsg, existing)
# Unregister app-specific if existing
if code in cls._func_hash and _is_app_specific_func(code):
cls.unregister(code)
cls._do_register(code, name, hash_name, hash_new)
[docs]
@classmethod
def unregister(cls, code: int) -> None:
"""Remove an application-specific function from the registry."""
if not _is_app_specific_func(code):
raise ValueError("only application-specific functions can be unregistered")
func_names = {n for n, f in cls._func_from_name.items() if f == code}
for func_name in func_names:
del cls._func_from_name[func_name]
hash_data = cls._func_hash.pop(code)
if hash_data.name:
del cls._func_from_hash[hash_data.name]
[docs]
@classmethod
def func_from_hash(cls, hash_obj) -> Func | int:
"""Return the multihash Func for a hashlib-compatible hash object.
Args:
hash_obj: Hashlib-compatible hash object
Returns:
Func enum or int code
Raises:
KeyError: If hash object name is not registered
"""
try:
return cls._func_from_hash[hash_obj.name]
except KeyError:
raise KeyError(f"unknown hash object name: {hash_obj.name}")
[docs]
@classmethod
def hash_from_func(cls, func: Func | int, length: int | None = None):
"""Return a hashlib-compatible object for the multihash func.
Args:
func: Hash function code or Func enum
length: Optional length for SHAKE hashes. Required for SHAKE. Returns None if None for SHAKE.
Returns:
Hash object or None if not available
Note:
SHAKE functions (shake_128, shake_256) require a length parameter
to specify the output digest size. If length is None for SHAKE
functions, this method returns None.
"""
new = cls._func_hash[func].new
if new is None:
# Handle SHAKE functions with variable length
if func == Func.shake_128:
if length is None:
return None
return ShakeHash(hashlib.shake_128, length)
elif func == Func.shake_256:
if length is None:
return None
return ShakeHash(hashlib.shake_256, length)
return None
return new()
# Initialize the function hash registry.
FuncReg.reset()