|
1 | 1 | """
|
2 | 2 | Demonstrates implementation of SHA1 Hash function in a Python class and gives utilities
|
3 | 3 | to find hash of string or hash of text from a file.
|
4 |
| -Usage: python sha1.py --string "Hello World Welcome to Cryptography" |
| 4 | +Usage: python sha1.py --string "Hello World!!" |
5 | 5 | pyhton sha1.py --file "hello_world.txt"
|
6 |
| - Without any arguments prints the hash of the string "Hello World" |
| 6 | + When run without any arguments, it prints the hash of the string "Hello World!! Welcome to Cryptography" |
7 | 7 | Also contains a Test class to verify that the generated Hash is same as that
|
8 | 8 | returned by the hashlib library
|
9 | 9 |
|
| 10 | +SHA1 hash or SHA1 sum of a string is a crytpographic function which means it is easy |
| 11 | +to calculate forwards but extemely difficult to calculate backwards. What this means |
| 12 | +is, you can easily calculate the hash of a string, but it is extremely difficult to |
| 13 | +know the original string if you have its hash. This property is useful to communicate |
| 14 | +securely, send encrypted messages and is very useful in payment systems, blockchain |
| 15 | +and cryptocurrency etc. |
10 | 16 | The Algorithm as described in the reference:
|
11 | 17 | First we start with a message. The message is padded and the length of the message
|
12 |
| -is added to the end. It is then split into blocks of 512 bits. The blocks are then |
13 |
| -processed one at a time. Each block must be expanded and compressed. |
| 18 | +is added to the end. It is then split into blocks of 512 bits or 64 bytes. The blocks |
| 19 | +are then processed one at a time. Each block must be expanded and compressed. |
14 | 20 | The value after each compression is added to a 160bit buffer called the current hash
|
15 | 21 | state. After the last block is processed the current hash state is returned as
|
16 | 22 | the final hash.
|
17 | 23 | Reference: https://deadhacker.com/2006/02/21/sha-1-illustrated/
|
18 | 24 | """
|
19 | 25 |
|
20 | 26 | import argparse
|
| 27 | +import struct |
21 | 28 | import hashlib #hashlib is only used inside the Test class
|
22 |
| - |
| 29 | +import unittest |
23 | 30 |
|
24 | 31 |
|
25 | 32 | class SHA1Hash:
|
26 | 33 | """
|
27 | 34 | Class to contain the entire pipeline for SHA1 Hashing Algorithm
|
28 | 35 | """
|
29 |
| - |
30 |
| - H0 - 01100111010001010010001100000001 |
31 |
| - H1 - 11101111110011011010101110001001 |
32 |
| - H2 - 10011000101110101101110011111110 |
33 |
| - H3 - 00010000001100100101010001110110 |
34 |
| - H4 - 11000011110100101110000111110000 |
35 | 36 |
|
36 | 37 | def __init__(self, data):
|
| 38 | + """ |
| 39 | + Inititates the variables data and h. h is a list of 5 8-digit Hexadecimal |
| 40 | + numbers corresponding to (1732584193, 4023233417, 2562383102, 271733878, 3285377520) |
| 41 | + respectively. We will start with this as a message digest. 0x is how you write |
| 42 | + Hexadecimal numbers in Python |
| 43 | + """ |
37 | 44 | self.data = data
|
38 |
| - self.current_hash = '' |
39 |
| - |
40 |
| - def padding(self): |
41 |
| - return |
42 |
| - |
43 |
| - def split_block(self): |
44 |
| - return |
| 45 | + self.h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] |
45 | 46 |
|
46 |
| - def expand_block(self): |
47 |
| - return |
| 47 | + @staticmethod |
| 48 | + def rotate(n, b): |
| 49 | + """ |
| 50 | + Static method to be used inside other methods. Left rotates n by b. |
| 51 | + """ |
| 52 | + return ((n << b) | (n >> (32 - b))) & 0xffffffff |
48 | 53 |
|
49 |
| - def compress_block(self): |
50 |
| - return |
| 54 | + def padding(self): |
| 55 | + """ |
| 56 | + Pads the input message with zeros so that padded_data has 64 bytes or 512 bits |
| 57 | + """ |
| 58 | + padding = b'\x80' + b'\x00'*(63 - (len(self.data) + 8) % 64) |
| 59 | + padded_data = self.data + padding + struct.pack('>Q', 8 * len(self.data)) |
| 60 | + return padded_data |
| 61 | + |
| 62 | + def split_blocks(self): |
| 63 | + """ |
| 64 | + Returns a list of bytestrings each of length 64 |
| 65 | + """ |
| 66 | + return [self.padded_data[i:i+64] for i in range(0, len(self.padded_data), 64)] |
| 67 | + |
| 68 | + # @staticmethod |
| 69 | + def expand_block(self, block): |
| 70 | + """ |
| 71 | + Takes block of 64 and returns list of length 80. |
| 72 | + It is really a static method but |
| 73 | + we need the rotate method inside, so we will have to use self |
| 74 | + """ |
| 75 | + w = list(struct.unpack('>16L', block)) + [0] * 64 |
| 76 | + for i in range(16, 80): |
| 77 | + w[i] = self.rotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1) |
| 78 | + return w |
51 | 79 |
|
52 | 80 | def final_hash(self):
|
53 |
| - return 'This is in my To Do list' |
54 |
| - |
55 |
| -class SHA1HashTest: |
| 81 | + """ |
| 82 | + Calls all the other methods to process the input. Returns SHA1 hash |
| 83 | + """ |
| 84 | + self.padded_data = self.padding() |
| 85 | + self.blocks = self.split_blocks() |
| 86 | + for block in self.blocks: |
| 87 | + expanded_block = self.expand_block(block) |
| 88 | + a, b, c, d, e = self.h |
| 89 | + for i in range(0, 80): |
| 90 | + if 0 <= i < 20: |
| 91 | + f = (b & c) | ((~b) & d) |
| 92 | + k = 0x5A827999 |
| 93 | + elif 20 <= i < 40: |
| 94 | + f = b ^ c ^ d |
| 95 | + k = 0x6ED9EBA1 |
| 96 | + elif 40 <= i < 60: |
| 97 | + f = (b & c) | (b & d) | (c & d) |
| 98 | + k = 0x8F1BBCDC |
| 99 | + elif 60 <= i < 80: |
| 100 | + f = b ^ c ^ d |
| 101 | + k = 0xCA62C1D6 |
| 102 | + a, b, c, d, e = self.rotate(a, 5) + f + e + k + expanded_block[i] & 0xffffffff,\ |
| 103 | + a, self.rotate(b, 30), c, d |
| 104 | + self.h = self.h[0] + a & 0xffffffff,\ |
| 105 | + self.h[1] + b & 0xffffffff,\ |
| 106 | + self.h[2] + c & 0xffffffff,\ |
| 107 | + self.h[3] + d & 0xffffffff,\ |
| 108 | + self.h[4] + e & 0xffffffff |
| 109 | + |
| 110 | + return '%08x%08x%08x%08x%08x' %tuple(self.h) |
| 111 | + |
| 112 | + |
| 113 | +class SHA1HashTest(unittest.TestCase): |
56 | 114 | """
|
57 |
| - Test class for the SHA1 class |
| 115 | + Test class for the SHA1Hash class. Inherits the TestCase class from unittest |
58 | 116 | """
|
59 |
| - def __init__(self, data): |
60 |
| - self.data = data |
| 117 | + def testMatchHashes(self): |
| 118 | + msg = bytes("Hello World", 'utf-8') |
| 119 | + self.assertEqual(SHA1Hash(msg).final_hash(), hashlib.sha1(msg).hexdigest()) |
61 | 120 |
|
62 |
| - def calculated_hash(self): |
63 |
| - return SHA1Hash(self.data).final_hash() |
64 |
| - |
65 |
| - def hashlib_hash(self): |
66 |
| - return hashlib.sha1(self.data.byte_encode()).hexdigest() |
67 |
| - |
68 |
| - def byte_encode(self): |
69 |
| - return bytes(self.data, 'utf-8') |
70 |
| - |
71 |
| - def match_hashes(self): |
72 |
| - # self.assertEqual(self.calculated_hash(), self.hashlib_hash()) |
73 |
| - return self.calculated_hash() == self.hashlib_hash() |
74 |
| - |
75 |
| -def run_test_case(hash_input = 'Hello World'): |
| 121 | +def run_test(): |
76 | 122 | """
|
77 |
| - Pulled this out of main because we probably dont want to run the Test |
78 |
| - each time we want to calculate hash. |
| 123 | + Run the unit test. Pulled this out of main because we probably dont want to run |
| 124 | + the test each time. |
79 | 125 | """
|
80 |
| - print(SHA1HashTest(hash_input).match_hashes()) |
81 |
| - |
| 126 | + unittest.main() |
82 | 127 |
|
83 | 128 | def main():
|
| 129 | + """ |
| 130 | + Provides option string or file to take input and prints the calculated SHA1 hash |
| 131 | + """ |
84 | 132 | parser = argparse.ArgumentParser(description='Process some strings or files')
|
85 |
| - parser.add_argument('--string', dest='input_string', default='Hello World', |
| 133 | + parser.add_argument('--string', dest='input_string', |
| 134 | + default='Hello World!! Welcome to Cryptography', |
86 | 135 | help='Hash the string')
|
87 | 136 | parser.add_argument('--file', dest='input_file', help='Hash contents of a file')
|
88 | 137 | args = parser.parse_args()
|
89 | 138 | input_string = args.input_string
|
| 139 | + #In any case hash input should be a bytestring |
90 | 140 | if args.input_file:
|
91 |
| - hash_input = open(args.input_file, 'r').read() |
| 141 | + hash_input = open(args.input_file, 'rb').read() |
92 | 142 | else:
|
93 |
| - hash_input = input_string |
| 143 | + hash_input = bytes(input_string, 'utf-8') |
94 | 144 | print(SHA1Hash(hash_input).final_hash())
|
95 | 145 |
|
96 | 146 | if __name__ == '__main__':
|
|
0 commit comments