diff --git a/bcrypt/__init__.py b/bcrypt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bcrypt/cli/__init__.py b/bcrypt/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bcrypt/bcrypt.py b/bcrypt/debug.py similarity index 65% rename from bcrypt/bcrypt.py rename to bcrypt/debug.py index 29a373b..a76e54e 100644 --- a/bcrypt/bcrypt.py +++ b/bcrypt/debug.py @@ -1,54 +1,6 @@ from math import gcd from random import randint, randbytes -H = 18446744073709551614 -K = 59275109328752 -M = 2**64 - -def Rjb(j: int, b: int) -> int: - return b << j**2 - -def Rb(b: int) -> int: - Rb = 0 - for j in range(8): - Rb ^= Rjb(j, b) - return Rb - -''' -Returns a hashmap (python dictionary) of b -> R(b) -for all b such that: min < b < max -''' -def precompute_R(min: int, - max: int) -> dict[int, int]: - b = min - R_table = {} - while b <= max: - R_table[b] = Rb(b) - b += 1 - return R_table - - -def hashfn(x: bytes, R_table: dict[int, int]) -> int: - h = -2 - for i in range(len((x))): - b = x[i] - h = h**2 * (b+1) + ((K * (i+1)) ^ R_table[b]) - h %= M - return h % M - -def bcrypt(x: bytes) -> int: - h = 18446744073709551614 - - for (i, b) in enumerate(x): - h *= h * (b + 1) - k = 59275109328752 * (i + 1) - for j in range(8): - k ^= b << (j * j) - h += k - h %= (2 ** 64) - - return h - def debug_hashes_eq(x: bytes, R_table: dict[int, int]) -> bool: return hashfn(x, R_table) == bcrypt(x) diff --git a/bcrypt/debug/__init__.py b/bcrypt/debug/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bcrypt/debug/constants.py b/bcrypt/debug/constants.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bcrypt/debug/constants.py @@ -0,0 +1 @@ + diff --git a/bcrypt/debug/constituents.py b/bcrypt/debug/constituents.py new file mode 100644 index 0000000..051e53f --- /dev/null +++ b/bcrypt/debug/constituents.py @@ -0,0 +1,18 @@ +''' +This file provides various "disint" (display internal) methods +regarding the "constituent functions" that exist in "bcrypt". +''' + +from bcrypt.lib.format import lpad, lpadbin + +''' +Display internal calculations of Rb(b: int) for bcrypt/hashrev.py +''' +def disint_Rb(b: int): + Rb = 0 + for j in range(8): + j_sq = j**2 + Rjb = b << j_sq + Rb ^= Rjb + print(f'{lpad(j_sq, 2)}: {lpadbin(Rjb, 64)} {Rjb}') + print(f'Rb: {lpadbin(Rb, 64)} {Rb}') diff --git a/bcrypt/debug/hasheq.py b/bcrypt/debug/hasheq.py new file mode 100644 index 0000000..4d17b42 --- /dev/null +++ b/bcrypt/debug/hasheq.py @@ -0,0 +1,39 @@ +''' +This file implements debug methods to check whether +hashrev.bcrypt() operates identically to hash.bcrypt(). +TLDR: test if I made a dumb typo. +''' + +from random import randint, randbytes +import bcrypt.hash +import bcrypt.hashrev + +def is_bcrypt_eq(x: bytes, R_table: Optional[dict[int, int]] = None) -> bool: + return hashrev.bcrypt(x, R_table=R_table) == hash.bcrypt(x) + +''' +Display internals of testing bcrypt implementations +from bcrypt/hash.py and bcrypt/hashrev.py +NOTE: By default this runs 10000 trials for +NOTE: random sequences of length 0-16 bytes. +''' +def disint_bcrypt_eq(trials: int = 10000, + max_bytes: int = 16) -> bytes | None: + R_table = hashrev.calc_R_array(max = 255) + for i in range(trials): + # generate random bytes + num_bytes = randint(0, max_bytes) + x = randbytes(num_bytes) + + # test the modified bcrypt with the original + hash_rev = hashrev.bcrypt(x, R_table=R_table) + hash_expected = hash.bcrypt(x) + if hash_rev != hash_expected: + print(f'Your hashfn sucks, big mistake bucko!! (failed iter: {i})') + print(f' Got: {hash_rev}') + print(f'Expected: {hash_bcrypt}') + print(f'Input Bytes: {[str(b) for b in x]}') + return x + + print('Impeccable hashfn holy moly!!') + return None diff --git a/bcrypt/hash/__init__.py b/bcrypt/hash/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bcrypt/hash/og.py b/bcrypt/hash/og.py new file mode 100644 index 0000000..d9034e8 --- /dev/null +++ b/bcrypt/hash/og.py @@ -0,0 +1,15 @@ +''' +The unchanged original "bcrypt" function for the CTF (by bpaul) +''' +def bcrypt(x: bytes) -> int: + h = 18446744073709551614 + + for (i, b) in enumerate(x): + h *= h * (b + 1) + k = 59275109328752 * (i + 1) + for j in range(8): + k ^= b << (j * j) + h += k + h %= (2 ** 64) + + return h diff --git a/bcrypt/hash/rev.py b/bcrypt/hash/rev.py new file mode 100644 index 0000000..0242b0c --- /dev/null +++ b/bcrypt/hash/rev.py @@ -0,0 +1,59 @@ +''' +This file reverse engineers and reconstructs the original +"bcrypt" hash function for the CTF (stored in bcrypt/hash.py). +This reconstructs implements, namely, precomputation of tables +of values in the hashing. Allowing bulk computation of hashes +to be significantly faster. +''' + +from array import array +from typing import Optional + +# Constants +H = 18446744073709551614 +K = 59275109328752 +M = 2**64 + +def Rjb(j: int, b: int) -> int: + return b << j**2 + +def Rb(b: int) -> int: + Rb = 0 + for j in range(8): + Rb ^= Rjb(j, b) + return Rb + +''' +Reconstructed implementation of the "bcrypt" hash function by bpaul. +NOTE: An optional precomputed R_table is permitted as an arguement. +''' +def bcrypt(x: bytes, R_table: Optional[dict[int, int]] = None) -> int: + h = -2 + for i, b in enumerate(x): + R = R_table[b] if R_table is not None else Rb(b) + h = h**2 * (b+1) + ((K * (i+1)) ^ R) + h %= M + return h % M + +''' +Returns a hashmap (python dictionary) of b -> R(b) +for all b such that: min <= b <= max +''' +def calc_R_map(min: int = 0, + max: int = 255) -> dict[int, int]: + R_map = {} + # b from min to max (inclusive) + for b in range(min, max+1): + R_map[b] = Rb(b) + return R_map + +''' +Same as calc_R_map() but using an array not a hashmap. +''' +def calc_R_array(min: int = 0, + max: int = 255) -> array.array[int]: + R_array = array('i', [0]*(max-min)) + # b from min to max (inclusive) + for b in range(min, max+1): + R_array[b-min] = Rb(b) + return R_array diff --git a/bcrypt/lib/format.py b/bcrypt/lib/format.py new file mode 100644 index 0000000..1a5f608 --- /dev/null +++ b/bcrypt/lib/format.py @@ -0,0 +1,17 @@ +from typing import Any +from bcrypt.lib.math import clamp_min + +''' +Apply left padding to str(x) for parameter x: Any +''' +def lpad(x: Any, n: int, pad: chr = ' ') -> str: + x = str(x) + width = clamp_min(n - len(x), 0) + return width * pad + x + +''' +Left pad an integer's binary representation with zeros +''' +def lpadbin(x: int, n: int) -> str: + return lpad(bin(x)[2:], n, pad='0') + diff --git a/bcrypt/lib/math.py b/bcrypt/lib/math.py new file mode 100644 index 0000000..23a82f6 --- /dev/null +++ b/bcrypt/lib/math.py @@ -0,0 +1,16 @@ +''' +Implements numeric range clamping (idk why its not in the stdlib...) +NOTE: the upper and lower bounds for clamping are inclusive +''' +def clamp(x: int, min: int, max: int) -> int: + if x < min: + return min + elif x > max: + return max + return x + +def clamp_min(x: int, min: int) -> int: + return x if x > min else min + +def clamp_max(x: int, max: int) -> int: + return x if x < max else max diff --git a/bcrypt/working/shifts.py b/bcrypt/working/shifts.py deleted file mode 100644 index dcdbe37..0000000 --- a/bcrypt/working/shifts.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Any - -def clamp_pos(x: int): - return x if x > 0 else 0 - -def lpad(x: Any, n: int, pad: chr = ' '): - x = str(x) - return clamp_pos(n - len(x))*pad + x - -def debug_R(B: int): - # B = int('1'*7, 2) - for j in range(8): - j_sq = j**2 - R_j = B << j_sq - lpadbin_R_j = lpad(bin(R_j)[2:], 64, pad='0') - print(f'{lpad(j_sq, 2)}: {lpadbin_R_j} {R_j}') - -def main(): - B = int(input('B: '), 2) - debug_R(B) - -if __name__ == '__main__': - main() diff --git a/bcrypter.py b/bcrypter.py new file mode 100644 index 0000000..583ade6 --- /dev/null +++ b/bcrypter.py @@ -0,0 +1,12 @@ +from bcrypt.cli import repl + +def main(): + repl() + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print('\n[!] SIGINT') + except EOFError: + print('\n[!] EOF')