Created
September 30, 2022 09:13
-
-
Save ObjSal/c8e2583c17127af96b8219e8768c7f5a to your computer and use it in GitHub Desktop.
Hashcash
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
# https://en.wikipedia.org/wiki/Hashcash | |
# Author: https://twitter.com/ObjSal | |
import hashlib | |
import base64 | |
from math import ceil | |
import re | |
import datetime | |
''' | |
X-Hashcash: 1:20:2209300908:ObjSal@twitter::QE9ialNhbA:NP7f | |
The header contains: | |
ver: Hashcash format version, 1 (which supersedes version 0). | |
bits: Number of "partial pre-image" (zero) bits in the hashed code. | |
date: The time that the message was sent, in the format YYMMDD[hhmm[ss]]. | |
resource: Resource data string being transmitted, e.g., an IP address or email address. | |
ext: Extension (optional; ignored in version 1). | |
rand: String of random characters, encoded in base-64 format. | |
counter: Binary counter, encoded in base-64 format. | |
''' | |
def validate_pattern(hashcash): | |
# check if hashcash format is valid | |
# \d+ -> one or more digits | |
# [^:] -> any character that is not : | |
pattern = re.compile(r'^(\d+:){3}[^:]+:[^:]*(:[^:]+){2}$') | |
if not pattern.fullmatch(hashcash): | |
raise Exception('hashcash format is invalid') | |
def get_pre_image(hashcash): | |
# pre-image is the second number in the hashcash | |
first_colon = hashcash.find(':') + 1 | |
second_colon = hashcash.find(':', first_colon) | |
bits = hashcash[first_colon : second_colon] | |
leading_zeros_count = int(bits) / 4 | |
pre_image_v = ['0'] * int(leading_zeros_count) | |
return ''.join(pre_image_v) | |
def sha1(hashcash): | |
hash = hashlib.sha1() | |
hash.update(bytes(hashcash, 'utf-8')) | |
hexdigest = hash.hexdigest() | |
# print(hexdigest) | |
return hexdigest | |
def verify(hashcash): | |
validate_pattern(hashcash) | |
# get the number of leading zeros expected from the hashcash | |
pre_image = get_pre_image(hashcash) | |
# re-create the hash to validate hashcash | |
hashcash_hash = sha1(hashcash) | |
# check if the leading zeros matches the specified number in the hashcash | |
if not hashcash_hash.startswith(pre_image): | |
raise Exception("Number of 'partial pre-image' (zero) bits in the hashed code is different than expected") | |
# print(hashcash_hash) | |
return True | |
''' | |
Create the hashtash header then computes the 160-bit SHA-1 hash. | |
If the first 20 bits (i.e. the 5 most significant hex digits) of the hash are all zeros, | |
then this is an acceptable header. If not, then it increments the counter and tries the hash again. | |
Out of 2^160 possible hash values, there are 2^140 hash values that satisfy this criterion. | |
Thus the chance of randomly selecting a header that will have 20 zeros as the beginning of the hash | |
is 1 in 2^20 (approx. 10^6, or about one in a million). | |
''' | |
def create_hashtash(recipient): | |
# ver: Hashcash format version, 1 (which supersedes version 0). | |
# bits: Number of "partial pre-image" (zero) bits in the hashed code. | |
# date: The time that the message was sent, in the format YYMMDD[hhmm[ss]]. | |
# resource: Resource data string being transmitted, e.g., an IP address or email address. | |
# ext: Extension (optional; ignored in version 1). | |
# rand: String of random characters, encoded in base-64 format. | |
# counter: Binary counter, encoded in base-64 format. | |
ver = '1' | |
bits = '20' | |
date = datetime.datetime.utcnow().strftime('%y%m%d%H%M') | |
resource = recipient | |
ext = '' | |
rand = base64.b64encode(b'@ObjSal').decode("utf-8").rstrip('=') | |
counter = 0 | |
pre = [ver, bits, date, resource, ext, rand] | |
pre_hashcash = ':'.join(pre) + ':' | |
valid = False | |
result = '' | |
while not valid: | |
try: | |
length = max(1, ceil(counter.bit_length() / 8)) | |
counter_bytes = counter.to_bytes(int(length), byteorder='big', signed=False) | |
counter_str = base64.b64encode(counter_bytes).decode("utf-8").rstrip('=') | |
result = pre_hashcash + counter_str | |
valid = verify(result) | |
except: | |
pass | |
finally: | |
counter += 1 | |
return result | |
def main(): | |
hashcash = create_hashtash("ObjSal@twitter") | |
print(hashcash) | |
if verify(hashcash): | |
print("Valid") | |
else: | |
print("Invalid") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment