-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import hashlib | ||
|
||
from .compat import OrderedDict, bytearray_or_str | ||
|
||
from .length import Length | ||
|
||
|
||
def decode_hash_algorithm(octet): | ||
if not isinstance(octet, int) or not 0 <= octet < 256: | ||
raise TypeError('Bad octet value: `{0}` of type `{1}`'.format( | ||
octet, type(octet))) | ||
|
||
try: | ||
return BYTE_TO_HASH[octet] | ||
except KeyError: | ||
raise ValueError( | ||
'Unknown hash algorithm `{0}`. See ' | ||
'http://tools.ietf.org/html/rfc4880#section-9.4'.format(octet)) | ||
|
||
|
||
class HashAlgorithm(): | ||
""" | ||
9.4. Hash Algorithms | ||
http://tools.ietf.org/html/rfc4880#section-9.4 | ||
""" | ||
def __init__(self): | ||
raise RuntimeError('HashAlgorithm should not be instantiated') | ||
|
||
@classmethod | ||
def new(cls): | ||
return HashWrapper(cls.hash_constructor()) | ||
|
||
@classmethod | ||
def serialize(cls): | ||
return OrderedDict([ | ||
('name', cls.__name__), | ||
('octet_value', HASH_TO_BYTE[cls]), | ||
('digest_length', cls.length), | ||
]) | ||
|
||
|
||
class HashWrapper(): | ||
def __init__(self, hash_instance): | ||
self._h = hash_instance | ||
|
||
def update(self, data): | ||
return self._h.update(bytearray_or_str(data)) | ||
|
||
def digest(self): | ||
return bytearray(self._h.digest()) | ||
|
||
def hexdigest(self): | ||
return self._h.hexdigest() | ||
|
||
|
||
class MD5(HashAlgorithm): | ||
length = Length(bits=128) # 16 octets | ||
hash_constructor = hashlib.md5 | ||
|
||
|
||
class SHA1(HashAlgorithm): | ||
length = Length(bits=160) # 20 octets | ||
hash_constructor = hashlib.sha1 | ||
|
||
|
||
class RIPEMD160(HashAlgorithm): | ||
length = Length(bits=160) # 20 octets | ||
|
||
@staticmethod | ||
def hash_constructor(): | ||
return hashlib.new('ripemd160') | ||
|
||
|
||
class SHA256(HashAlgorithm): | ||
length = Length(bits=256) # 32 octets | ||
hash_constructor = hashlib.sha256 | ||
|
||
|
||
class SHA384(HashAlgorithm): | ||
length = Length(bits=384) # 48 octets | ||
hash_constructor = hashlib.sha384 | ||
|
||
|
||
class SHA512(HashAlgorithm): | ||
length = Length(bits=512) # 64 octets | ||
hash_constructor = hashlib.sha512 | ||
|
||
|
||
class SHA224(HashAlgorithm): | ||
length = Length(bits=224) # 28 octets | ||
hash_constructor = hashlib.sha224 | ||
|
||
|
||
BYTE_TO_HASH = { | ||
1: MD5, | ||
2: SHA1, | ||
3: RIPEMD160, | ||
8: SHA256, | ||
9: SHA384, | ||
10: SHA512, | ||
11: SHA224, | ||
} | ||
|
||
HASH_TO_BYTE = dict( | ||
[(v, k) for k, v in BYTE_TO_HASH.items()] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from .compat import OrderedDict | ||
|
||
|
||
class Length(object): | ||
def __init__(self, bits=None, octets=None): | ||
if not self.ensure_exactly_one_non_null(bits, octets): | ||
raise ValueError('Exactly one of `bits` and `octets` required.') | ||
|
||
if bits is not None: | ||
if bits % 8 != 0: | ||
raise ValueError('Bits must be a multiple of 8') | ||
self._length_bits = bits | ||
|
||
elif octets is not None: | ||
self._length_bits = octets * 8 | ||
|
||
@staticmethod | ||
def ensure_exactly_one_non_null(*args): | ||
non_nulls = list(filter(lambda x: x is not None, args)) | ||
return len(non_nulls) == 1 | ||
|
||
@property | ||
def in_bits(self): | ||
return self._length_bits | ||
|
||
@property | ||
def in_octets(self): | ||
return int(self._length_bits / 8) | ||
|
||
def serialize(self): | ||
return OrderedDict([ | ||
('bits', self.in_bits), | ||
('octets', self.in_octets), | ||
]) |
Empty file.
32 changes: 32 additions & 0 deletions
32
encryptit/tests/hash_algorithms/test_decode_hash_algorithm.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from nose.tools import assert_equal, assert_raises | ||
|
||
from encryptit.hash_algorithms import ( | ||
decode_hash_algorithm, MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, | ||
SHA224) | ||
|
||
HASH_ALGOS = { | ||
# https://tools.ietf.org/html/rfc4880#section-9.4 | ||
1: MD5, | ||
2: SHA1, | ||
3: RIPEMD160, | ||
8: SHA256, | ||
9: SHA384, | ||
10: SHA512, | ||
11: SHA224, | ||
} | ||
|
||
|
||
def test_decode_returns_correct_hash_class(): | ||
for octet, expected_class in HASH_ALGOS.items(): | ||
got_class = decode_hash_algorithm(octet) | ||
yield assert_equal, expected_class, got_class | ||
|
||
|
||
def test_decode_raises_value_error_on_all_other_octet_values(): | ||
for bad_octet in set(range(256)) - set(HASH_ALGOS.keys()): | ||
yield assert_raises, ValueError, decode_hash_algorithm, bad_octet | ||
|
||
|
||
def test_decode_raises_type_error_for_invalid_octet(): | ||
for not_octet in [-1, 256, 1.0, '1']: | ||
yield assert_raises, TypeError, decode_hash_algorithm, not_octet |
72 changes: 72 additions & 0 deletions
72
encryptit/tests/hash_algorithms/test_hash_algorithm_fields.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# You can test the hashes here on the command line: | ||
# $ dd if=/dev/zero bs=1 count=1 | sha224sum | ||
|
||
from collections import namedtuple | ||
|
||
from nose.tools import assert_equal | ||
|
||
from encryptit.hash_algorithms import ( | ||
MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224) | ||
|
||
HashDefinition = namedtuple( | ||
'HashDefinition', | ||
'name,digest_bits,zero_hashed') | ||
|
||
KNOWN_PROPERTIES = { | ||
# https://en.wikipedia.org/wiki/MD5 | ||
MD5: HashDefinition( | ||
'MD5', | ||
128, | ||
'93b885adfe0da089cdf634904fd59f71'), | ||
|
||
# https://en.wikipedia.org/wiki/SHA-1 | ||
SHA1: HashDefinition( | ||
'SHA1', | ||
160, | ||
'5ba93c9db0cff93f52b521d7420e43f6eda2784f'), | ||
|
||
# https://en.wikipedia.org/wiki/RIPEMD | ||
# dd if=/dev/zero bs=1 count=1 | openssl rmd160 | ||
RIPEMD160: HashDefinition( | ||
'RIPEMD160', | ||
160, | ||
'c81b94933420221a7ac004a90242d8b1d3e5070d'), | ||
|
||
SHA256: HashDefinition( | ||
'SHA256', | ||
256, | ||
'6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d'), | ||
|
||
SHA384: HashDefinition( | ||
'SHA384', | ||
384, | ||
'bec021b4f368e3069134e012c2b4307083d3a9bdd206e24e5f0d86e13d6636655933' | ||
'ec2b413465966817a9c208a11717'), | ||
|
||
SHA512: HashDefinition( | ||
'SHA512', | ||
512, | ||
'b8244d028981d693af7b456af8efa4cad63d282e19ff14942c246e50d9351d22704a' | ||
'802a71c3580b6370de4ceb293c324a8423342557d4e5c38438f0e36910ee'), | ||
|
||
SHA224: HashDefinition( | ||
'SHA224', | ||
224, | ||
'fff9292b4201617bdc4d3053fce02734166a683d7d858a7f5f59b073'), | ||
} | ||
|
||
|
||
def test_hash_algorithms(): | ||
for hash_cls, expected in KNOWN_PROPERTIES.items(): | ||
yield assert_equal, expected.name, hash_cls.serialize()['name'] | ||
yield assert_equal, expected.digest_bits, hash_cls.length.in_bits | ||
yield assert_equal, expected.zero_hashed, _hash_zero(hash_cls) | ||
|
||
|
||
def _hash_zero(hash_cls): | ||
""" | ||
Construct the hash context and update it with a single zero byte. | ||
""" | ||
hasher = hash_cls.new() | ||
hasher.update(bytearray([0])) | ||
return hasher.hexdigest() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from hashlib import sha1 | ||
from nose.tools import assert_equal | ||
|
||
from ..test_utils import assert_is_instance | ||
|
||
from encryptit.hash_algorithms import HashWrapper | ||
|
||
|
||
SHA1_HASH_OF_ZERO = '5ba93c9db0cff93f52b521d7420e43f6eda2784f' | ||
|
||
|
||
def test_update_handles_bytearray_correctly(): | ||
h = HashWrapper(sha1()) | ||
h.update(bytearray([0])) | ||
assert_equal(SHA1_HASH_OF_ZERO, h.hexdigest()) | ||
|
||
|
||
def test_that_digest_method_returns_bytearray(): | ||
h = HashWrapper(sha1()) | ||
h.update(bytearray([0])) | ||
yield assert_is_instance, h.digest(), bytearray | ||
yield assert_equal, h.digest()[0:4], bytearray([0x5b, 0xa9, 0x3c, 0x9d]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from nose.tools import assert_equal, assert_raises | ||
|
||
from encryptit.length import Length | ||
|
||
|
||
def test_length_in_octets(): | ||
l = Length(octets=10) | ||
assert_equal(10, l.in_octets) | ||
assert_equal(10 * 8, l.in_bits) | ||
|
||
|
||
def test_length_in_bits(): | ||
l = Length(bits=80) | ||
assert_equal(10, l.in_octets) | ||
assert_equal(10 * 8, l.in_bits) | ||
|
||
|
||
def test_length_in_bits_must_be_multiple_of_8(): | ||
for bits in [1, 2, 3, 4, 5, 6, 7]: | ||
yield assert_raises, ValueError, lambda: Length(bits=bits) | ||
|
||
|
||
def test_both_bits_and_octets_cannot_be_given(): | ||
assert_raises(ValueError, lambda: Length(octets=8, bits=80)) | ||
|
||
|
||
def test_serialize(): | ||
assert_equal( | ||
{ | ||
'bits': 80, | ||
'octets': 10 | ||
}, | ||
Length(octets=10).serialize() | ||
) |