Skip to content

Commit

Permalink
Merge 1baf7fe into 1d9391d
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul M Furley committed Apr 12, 2015
2 parents 1d9391d + 1baf7fe commit 2251c67
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 0 deletions.
1 change: 1 addition & 0 deletions encryptit/compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

from .struct_unpack import struct_unpack
from .abstract_class_method import abstractclassmethod
from .bytearray_or_str import bytearray_or_str
108 changes: 108 additions & 0 deletions encryptit/hash_algorithms.py
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()]
)
34 changes: 34 additions & 0 deletions encryptit/length.py
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 encryptit/tests/hash_algorithms/test_decode_hash_algorithm.py
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 encryptit/tests/hash_algorithms/test_hash_algorithm_fields.py
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()
22 changes: 22 additions & 0 deletions encryptit/tests/hash_algorithms/test_hash_wrapper.py
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])
34 changes: 34 additions & 0 deletions encryptit/tests/test_length.py
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()
)

0 comments on commit 2251c67

Please sign in to comment.