|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +# |
| 4 | +# Copyright © 2019 James Seo <james@equiv.tech> (github.com/kangtastic). |
| 5 | +# |
| 6 | +# This file is released under the WTFPL, version 2 (wtfpl.net). |
| 7 | +# |
| 8 | +# md4.py: An implementation of the MD4 hash algorithm in pure Python 3. |
| 9 | +# |
| 10 | +# Description: Zounds! Yet another rendition of pseudocode from RFC1320! |
| 11 | +# Bonus points for the algorithm literally being from 1992. |
| 12 | +# |
| 13 | +# Usage: Why would anybody use this? This is self-rolled crypto, and |
| 14 | +# self-rolled *obsolete* crypto at that. DO NOT USE if you need |
| 15 | +# something "performant" or "secure". :P |
| 16 | +# |
| 17 | +# Anyway, from the command line: |
| 18 | +# |
| 19 | +# $ ./md4.py [messages] |
| 20 | +# |
| 21 | +# where [messages] are some strings to be hashed. |
| 22 | +# |
| 23 | +# In Python, use similarly to hashlib (not that it even has MD4): |
| 24 | +# |
| 25 | +# from .md4 import MD4 |
| 26 | +# |
| 27 | +# digest = MD4("BEES").hexdigest() |
| 28 | +# |
| 29 | +# print(digest) # "501af1ef4b68495b5b7e37b15b4cda68" |
| 30 | +# |
| 31 | +# |
| 32 | +# Sample console output: |
| 33 | +# |
| 34 | +# Testing the MD4 class. |
| 35 | +# |
| 36 | +# Message: b'' |
| 37 | +# Expected: 31d6cfe0d16ae931b73c59d7e0c089c0 |
| 38 | +# Actual: 31d6cfe0d16ae931b73c59d7e0c089c0 |
| 39 | +# |
| 40 | +# Message: b'The quick brown fox jumps over the lazy dog' |
| 41 | +# Expected: 1bee69a46ba811185c194762abaeae90 |
| 42 | +# Actual: 1bee69a46ba811185c194762abaeae90 |
| 43 | +# |
| 44 | +# Message: b'BEES' |
| 45 | +# Expected: 501af1ef4b68495b5b7e37b15b4cda68 |
| 46 | +# Actual: 501af1ef4b68495b5b7e37b15b4cda68 |
| 47 | +# |
| 48 | +import struct |
| 49 | + |
| 50 | + |
| 51 | +class MD4: |
| 52 | + """An implementation of the MD4 hash algorithm.""" |
| 53 | + |
| 54 | + width = 32 |
| 55 | + mask = 0xFFFFFFFF |
| 56 | + |
| 57 | + # Unlike, say, SHA-1, MD4 uses little-endian. Fascinating! |
| 58 | + h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476] |
| 59 | + |
| 60 | + def __init__(self, msg=None): |
| 61 | + """:param ByteString msg: The message to be hashed.""" |
| 62 | + if msg is None: |
| 63 | + msg = b"" |
| 64 | + |
| 65 | + self.msg = msg |
| 66 | + |
| 67 | + # Pre-processing: Total length is a multiple of 512 bits. |
| 68 | + ml = len(msg) * 8 |
| 69 | + msg += b"\x80" |
| 70 | + msg += b"\x00" * (-(len(msg) + 8) % 64) |
| 71 | + msg += struct.pack("<Q", ml) |
| 72 | + |
| 73 | + # Process the message in successive 512-bit chunks. |
| 74 | + self._process([msg[i : i + 64] for i in range(0, len(msg), 64)]) |
| 75 | + |
| 76 | + def __repr__(self): |
| 77 | + if self.msg: |
| 78 | + return f"{self.__class__.__name__}({self.msg:s})" |
| 79 | + return f"{self.__class__.__name__}()" |
| 80 | + |
| 81 | + def __str__(self): |
| 82 | + return self.hexdigest() |
| 83 | + |
| 84 | + def __eq__(self, other): |
| 85 | + return self.h == other.h |
| 86 | + |
| 87 | + def bytes(self): |
| 88 | + """:return: The final hash value as a `bytes` object.""" |
| 89 | + return struct.pack("<4L", *self.h) |
| 90 | + |
| 91 | + def hexbytes(self): |
| 92 | + """:return: The final hash value as hexbytes.""" |
| 93 | + return self.hexdigest().encode |
| 94 | + |
| 95 | + def hexdigest(self): |
| 96 | + """:return: The final hash value as a hexstring.""" |
| 97 | + return "".join(f"{value:02x}" for value in self.bytes()) |
| 98 | + |
| 99 | + def _process(self, chunks): |
| 100 | + for chunk in chunks: |
| 101 | + X, h = list(struct.unpack("<16I", chunk)), self.h.copy() |
| 102 | + |
| 103 | + # Round 1. |
| 104 | + Xi = [3, 7, 11, 19] |
| 105 | + for n in range(16): |
| 106 | + i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) |
| 107 | + K, S = n, Xi[n % 4] |
| 108 | + hn = h[i] + MD4.F(h[j], h[k], h[l]) + X[K] |
| 109 | + h[i] = MD4.lrot(hn & MD4.mask, S) |
| 110 | + |
| 111 | + # Round 2. |
| 112 | + Xi = [3, 5, 9, 13] |
| 113 | + for n in range(16): |
| 114 | + i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) |
| 115 | + K, S = n % 4 * 4 + n // 4, Xi[n % 4] |
| 116 | + hn = h[i] + MD4.G(h[j], h[k], h[l]) + X[K] + 0x5A827999 |
| 117 | + h[i] = MD4.lrot(hn & MD4.mask, S) |
| 118 | + |
| 119 | + # Round 3. |
| 120 | + Xi = [3, 9, 11, 15] |
| 121 | + Ki = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15] |
| 122 | + for n in range(16): |
| 123 | + i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4)) |
| 124 | + K, S = Ki[n], Xi[n % 4] |
| 125 | + hn = h[i] + MD4.H(h[j], h[k], h[l]) + X[K] + 0x6ED9EBA1 |
| 126 | + h[i] = MD4.lrot(hn & MD4.mask, S) |
| 127 | + |
| 128 | + self.h = [((v + n) & MD4.mask) for v, n in zip(self.h, h)] |
| 129 | + |
| 130 | + @staticmethod |
| 131 | + def F(x, y, z): |
| 132 | + return (x & y) | (~x & z) |
| 133 | + |
| 134 | + @staticmethod |
| 135 | + def G(x, y, z): |
| 136 | + return (x & y) | (x & z) | (y & z) |
| 137 | + |
| 138 | + @staticmethod |
| 139 | + def H(x, y, z): |
| 140 | + return x ^ y ^ z |
| 141 | + |
| 142 | + @staticmethod |
| 143 | + def lrot(value, n): |
| 144 | + lbits, rbits = (value << n) & MD4.mask, value >> (MD4.width - n) |
| 145 | + return lbits | rbits |
| 146 | + |
| 147 | + |
| 148 | +def main(): |
| 149 | + # Import is intentionally delayed. |
| 150 | + import sys |
| 151 | + |
| 152 | + if len(sys.argv) > 1: |
| 153 | + messages = [msg.encode() for msg in sys.argv[1:]] |
| 154 | + for message in messages: |
| 155 | + print(MD4(message).hexdigest()) |
| 156 | + else: |
| 157 | + messages = [b"", b"The quick brown fox jumps over the lazy dog", b"BEES"] |
| 158 | + known_hashes = [ |
| 159 | + "31d6cfe0d16ae931b73c59d7e0c089c0", |
| 160 | + "1bee69a46ba811185c194762abaeae90", |
| 161 | + "501af1ef4b68495b5b7e37b15b4cda68", |
| 162 | + ] |
| 163 | + |
| 164 | + print("Testing the MD4 class.") |
| 165 | + print() |
| 166 | + |
| 167 | + for message, expected in zip(messages, known_hashes): |
| 168 | + print("Message: ", message) |
| 169 | + print("Expected:", expected) |
| 170 | + print("Actual: ", MD4(message).hexdigest()) |
| 171 | + print() |
| 172 | + |
| 173 | + |
| 174 | +if __name__ == "__main__": |
| 175 | + try: |
| 176 | + main() |
| 177 | + except KeyboardInterrupt: |
| 178 | + pass |
0 commit comments