Created
October 4, 2017 16:36
-
-
Save PM2Ring/63be1b6f43c77c3b36b19d4190400215 to your computer and use it in GitHub Desktop.
An implementation of SHA-1 in pure Python 3
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
#!/usr/bin/env python3 | |
''' sha1_demo | |
An implementation of SHA-1 in pure Python 3 | |
Built from the pseudocode at | |
https://en.wikipedia.org/wiki/SHA-1#SHA-1_pseudocode | |
Written by PM 2Ring 2017.10.01 | |
''' | |
# SHA-1 initialization constants | |
h_init = 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 | |
# A bitmask of 32 '1' bits, so we can do | |
# 32 bit unsigned arithmetic in Python | |
mask = 0xffffffff | |
def leftrotate(x, n): | |
''' Left rotate 32 bit integer x by n bits ''' | |
return ((x << n) | (x >> (32 - n))) & mask | |
def sha1(s): | |
# Add padding bits and length to message | |
ml = len(s) << 3 | |
d = (447 - ml) % 512 | |
n = 1 << d | |
nlen = (1 + d) >> 3 | |
s += n.to_bytes(nlen, 'big') + ml.to_bytes(8, 'big') | |
# Initialise the hash | |
h = list(h_init) | |
# Process the message in successive 512-bit chunks | |
for chunk in zip(*[iter(s)] * 64): | |
# Break chunk into sixteen 32-bit big-endian words | |
w = [int.from_bytes(u, 'big') for u in zip(*[iter(chunk)] * 4)] | |
# Extend the sixteen 32-bit words into eighty 32-bit words | |
for i in range(16, 80): | |
x = (w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]) | |
w.append(leftrotate(x, 1)) | |
# Initialize hash value for this chunk | |
a, b, c, d, e = h | |
# Main loop | |
for i in range(80): | |
if 0 <= i <= 19: | |
f = (b & c) | ((b ^ mask) & d) | |
k = 0x5A827999 | |
elif 20 <= i <= 39: | |
f = b ^ c ^ d | |
k = 0x6ED9EBA1 | |
elif 40 <= i <= 59: | |
f = (b & c) | (b & d) | (c & d) | |
k = 0x8F1BBCDC | |
elif 60 <= i <= 79: | |
f = b ^ c ^ d | |
k = 0xCA62C1D6 | |
a, b, c, d, e = ( | |
(leftrotate(a, 5) + f + e + k + w[i]) & mask, | |
a, leftrotate(b, 30), c, d | |
) | |
# Add this chunk's hash to result so far | |
h = [(u + v) & mask for u, v in zip(h, (a, b, c, d, e))] | |
# Produce the final hash value | |
return b''.join([u.to_bytes(4, 'big') for u in h]) | |
def test(numtests=1000, random_seed=None): | |
''' Validate sha1 against the standard version in hashlib ''' | |
seed(random_seed) | |
s = b'' | |
for i in range(numtests): | |
h0 = std_sha1(s).digest() | |
h1 = sha1(s) | |
assert h0 == h1, i | |
# Prepend a random byte to s | |
s = bytes([randrange(256)]) + s | |
if i % 100 == 0: | |
print(' {}'.format(i), end='\r', flush=True) | |
print('\nok') | |
def main(): | |
''' Calculate SHA-1 hashes of all files named on the command | |
line, or read from stdin if no file names are given | |
''' | |
if len(sys.argv) == 1: | |
fname = '-' | |
data = sys.stdin.buffer.read() | |
h = sha1(data) | |
print('{} -'.format(h.hex())) | |
else: | |
for fname in sys.argv[1:]: | |
try: | |
data = Path(fname).read_bytes() | |
except IOError as e: | |
print(e) | |
else: | |
h = sha1(data) | |
print('{} {}'.format(h.hex(), fname)) | |
if __name__ == '__main__': | |
import sys | |
from pathlib import Path | |
from hashlib import sha1 as std_sha1 | |
from random import seed, randrange | |
main() | |
#test() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment