Skip to content

Instantly share code, notes, and snippets.

@ObjSal
Created September 30, 2022 09:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ObjSal/c8e2583c17127af96b8219e8768c7f5a to your computer and use it in GitHub Desktop.
Save ObjSal/c8e2583c17127af96b8219e8768c7f5a to your computer and use it in GitHub Desktop.
Hashcash
# 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