paivana_id.py (1910B)
1 #!/usr/bin/env python3 2 """Recompute what the paywall page computes, independently of paivana. 3 4 This mirrors src/frontend/paywall.js -- makePaivanaId(), 5 timestampRoundedToBuffer() and encodeCrock() -- because agreement between 6 the two ends IS the protocol: neither side ever sends the paivana ID to 7 the other, each derives it from its own copy of the inputs. 8 9 Usage: paivana_id.py <expiration-seconds> <website> [<nonce-hex>] 10 Prints: <nonce-crockford> <paivana-id> 11 """ 12 import base64 13 import hashlib 14 import os 15 import struct 16 import sys 17 18 # GNUnet's Crockford base32 alphabet (gnunet strings.c), which is what 19 # GNUNET_STRINGS_string_to_data() reads back on the daemon side. 20 ENC_TABLE = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" 21 22 23 def encode_crock(data: bytes) -> str: 24 out = [] 25 bit_buf = 0 26 num_bits = 0 27 pos = 0 28 size = len(data) 29 while pos < size or num_bits > 0: 30 if pos < size and num_bits < 5: 31 bit_buf = (bit_buf << 8) | data[pos] 32 pos += 1 33 num_bits += 8 34 if num_bits < 5: 35 bit_buf <<= 5 - num_bits 36 num_bits = 5 37 out.append(ENC_TABLE[(bit_buf >> (num_bits - 5)) & 31]) 38 num_bits -= 5 39 return "".join(out) 40 41 42 def make_paivana_id(cur_time: int, nonce: bytes, website: str) -> str: 43 buf = nonce + website.encode("utf-8") + b"\0" + struct.pack(">Q", cur_time * 1000 * 1000) 44 digest = hashlib.sha256(buf).digest() 45 tail = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=") 46 return f"{cur_time}-{tail}" 47 48 49 def main() -> int: 50 if len(sys.argv) not in (3, 4): 51 sys.stderr.write(__doc__) 52 return 2 53 cur_time = int(sys.argv[1]) 54 website = sys.argv[2] 55 nonce = bytes.fromhex(sys.argv[3]) if len(sys.argv) == 4 else os.urandom(16) 56 print(f"{encode_crock(nonce)} {make_paivana_id(cur_time, nonce, website)}") 57 return 0 58 59 60 if __name__ == "__main__": 61 sys.exit(main())