-
Notifications
You must be signed in to change notification settings - Fork 96
/
hash.py
103 lines (87 loc) · 3.35 KB
/
hash.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# pylint: disable=isinstance-second-argument-not-valid-type
from abc import abstractmethod
from collections.abc import Sequence
from hashlib import sha256
from typing import (
Iterable,
List,
Union,
Protocol,
runtime_checkable,
Sequence as TypedSequence,
)
from .constants import get_small_prime
from .utils import BYTE_ENCODING, BYTE_ORDER
from .group import (
ElementModPOrQ,
ElementModQ,
ElementModP,
)
@runtime_checkable
class CryptoHashable(Protocol):
"""
Denotes hashable
"""
@abstractmethod
def crypto_hash(self) -> ElementModQ:
"""
Generates a hash given the fields on the implementing instance.
"""
@runtime_checkable
class CryptoHashCheckable(Protocol):
"""
Checkable version of crypto hash
"""
@abstractmethod
def crypto_hash_with(self, encryption_seed: ElementModQ) -> ElementModQ:
"""
Generates a hash with a given seed that can be checked later against the seed and class metadata.
"""
# All the "atomic" types that we know how to hash.
CryptoHashableT = Union[CryptoHashable, ElementModPOrQ, str, int, None]
# "Compound" types that we know how to hash. Note that we're using Sequence, rather than List,
# because Sequences are read-only, and thus safely covariant. All this really means is that
# we promise never to mutate any list that you pass to hash_elems.
CryptoHashableAll = Union[
TypedSequence[CryptoHashableT],
CryptoHashableT,
]
def hash_elems(*a: CryptoHashableAll) -> ElementModQ:
"""
Given zero or more elements, calculate their cryptographic hash
using SHA256. Allowed element types are `ElementModP`, `ElementModQ`,
`str`, or `int`, anything implementing `CryptoHashable`, and lists
or optionals of any of those types.
:param a: Zero or more elements of any of the accepted types.
:return: A cryptographic hash of these elements, concatenated.
"""
h = sha256()
h.update("|".encode(BYTE_ENCODING))
for x in a:
# We could just use str(x) for everything, but then we'd have a resulting string
# that's a bit Python-specific, and we'd rather make it easier for other languages
# to exactly match this hash function.
if isinstance(x, (ElementModP, ElementModQ)):
hash_me = x.to_hex()
elif isinstance(x, CryptoHashable):
hash_me = x.crypto_hash().to_hex()
elif isinstance(x, str):
# strings are iterable, so it's important to handle them before list-like types
hash_me = x
elif isinstance(x, int):
hash_me = str(x)
elif not x:
# This case captures empty lists and None, nicely guaranteeing that we don't
# need to do a recursive call if the list is empty. So we need a string to
# feed in for both of these cases. "None" would be a Python-specific thing,
# so we'll go with the more JSON-ish "null".
hash_me = "null"
elif isinstance(x, (Sequence, List, Iterable)):
# The simplest way to deal with lists, tuples, and such are to crunch them recursively.
hash_me = hash_elems(*x).to_hex()
else:
hash_me = str(x)
h.update((hash_me + "|").encode(BYTE_ENCODING))
return ElementModQ(
int.from_bytes(h.digest(), byteorder=BYTE_ORDER) % get_small_prime()
)