From cf4b6a4016f35c8c8d289aba8dbd7e6542fa5a91 Mon Sep 17 00:00:00 2001
From: Markus Birth <mbirth@gmail.com>
Date: Sun, 23 Feb 2020 03:13:35 +0100
Subject: [PATCH] Added brute-force tool for A-codes.

---
 brute-a.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 82 insertions(+)
 create mode 100755 brute-a.py

diff --git a/brute-a.py b/brute-a.py
new file mode 100755
index 0000000..8c5772c
--- /dev/null
+++ b/brute-a.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from functools import reduce
+from itertools import product
+
+CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXY'
+
+def safe2real(code: str):
+    """Converts a code with unambiguous characters to the real code"""
+    code = code.upper()
+    code = code.replace('V', 'A')   # A might be confused with 4
+    code = code.replace('W', 'E')   # E might be confused with 3
+    code = code.replace('X', 'I')   # I might be confused with 1
+    code = code.replace('Y', 'O')   # O might be confused with 0
+    return code
+
+def generateChecksum(base_code):
+    if len(base_code) != 15:
+        raise RuntimeError("Bad argument to checksum")
+    # Mystery/bug: Why is this ord('7') in particular? 55, 0o55 and 0x55 are all unrelated to anything in base 33. The check looks like it's trying to do c - 'A', but does the weirdest thing instead.
+    cksum = reduce(lambda cksum, c: (cksum + (ord(c) - ord('0') if ord(c) <= ord('9') else ord(c) - ord('7'))) % 33, base_code, 0)
+    return CHARSET[cksum]
+
+def validate(code):
+    if len(code) != 16: return False
+    theirs = code[15]
+    mine = generateChecksum(code[0:15])
+    return (theirs == mine)
+
+def get_type(code: str) -> str:
+    if code[0].isnumeric():
+        return 'NUM'
+    elif code[0] == 'A':
+        return '_A_'
+    elif code[0] == 'B':
+        return '_B_'
+    elif code[0] == 'C':
+        return '_C_'
+    else:
+        return 'UNK'
+
+if __name__ == '__main__':
+    import sys
+    args = None
+    if len(sys.argv) == 1:
+        print("Syntax: {} [A0...]".format(sys.argv[0]))
+        print("        replace unidentified characters with *")
+        sys.exit(1)
+
+    code = sys.argv[1]
+    if len(code) != 16:
+        print("Code must be exactly 16 characters long.")
+        sys.exit(2)
+
+    if get_type(code) != '_A_':
+        print("Code must be starting with 'A'!")
+        sys.exit(3)
+
+    real_code = safe2real(code)
+
+    print("Actual code: {}".format(real_code))
+
+    unknowns = 0
+    for c in real_code:
+        if c == '*':
+            unknowns += 1
+
+    if unknowns == 0:
+        print("No unknown digits. Nothing to do.")
+        sys.exit(4)
+
+    print("{} unknown digits. {} combinations.".format(unknowns, len(CHARSET)**unknowns))
+
+    print("Valid codes:")
+
+    repstring = real_code.replace("*", "{}")
+    for p in product(CHARSET, repeat=unknowns):
+        test_code = repstring.format(*p)
+        isvalid = validate(test_code)
+        if isvalid:
+            print(test_code)