From cc7ca002561ac5927422efab37c393724334034b Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Mon, 24 Feb 2020 14:46:48 +0100 Subject: [PATCH] Move methods to library file. Used for brute-a.py for now. --- brute-a.py | 102 +++++++++++++++++-------------------------------- ninty/codes.py | 78 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 67 deletions(-) create mode 100755 ninty/codes.py diff --git a/brute-a.py b/brute-a.py index 8c5772c..4b63a65 100755 --- a/brute-a.py +++ b/brute-a.py @@ -1,82 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from functools import reduce +import sys from itertools import product +from ninty.codes import safe2real, real2safe, generate_checksum, validate, get_type, CHARSET -CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXY' +args = None +if len(sys.argv) == 1: + print("Syntax: {} [A0...]".format(sys.argv[0])) + print(" replace unidentified characters with *") + sys.exit(1) -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 +code = sys.argv[1] +if len(code) != 16: + print("Code must be exactly 16 characters long.") + sys.exit(2) -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] +code_type = get_type(code) +if not code_type in ['_A_', '_B_', '_C_']: + print("Code must be starting with 'A', 'B' or 'C'!") + sys.exit(3) -def validate(code): - if len(code) != 16: return False - theirs = code[15] - mine = generateChecksum(code[0:15]) - return (theirs == mine) +base = 30 +if code_type == '_A_': + base = 33 -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' +real_code = safe2real(code) -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) +print("Actual code: {}".format(real_code)) - code = sys.argv[1] - if len(code) != 16: - print("Code must be exactly 16 characters long.") - sys.exit(2) +unknowns = 0 +for c in real_code: + if c == '*': + unknowns += 1 - if get_type(code) != '_A_': - print("Code must be starting with 'A'!") - sys.exit(3) +if unknowns == 0: + print("No unknown digits. Nothing to do.") + sys.exit(4) - real_code = safe2real(code) +print("{} unknown digits. {} combinations.".format(unknowns, len(CHARSET)**unknowns)) - print("Actual code: {}".format(real_code)) +print("Valid codes:") - 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) +repstring = real_code.replace("*", "{}") +for p in product(CHARSET[:base], repeat=unknowns): + test_code = repstring.format(*p) + isvalid = validate(test_code) + if isvalid: + print(real2safe(test_code)) diff --git a/ninty/codes.py b/ninty/codes.py new file mode 100755 index 0000000..0a6b162 --- /dev/null +++ b/ninty/codes.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# https://www.reddit.com/r/SwitchHacks/comments/f7psrk/anatomy_of_an_eshop_card_code/ + +CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + +def safe2real(code: str) -> str: + """Converts a code with unambiguous characters to the real code""" + code = code.upper() + code_first = code[0] + if code_first != 'A': + 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 real2safe(code: str) -> str: + """Converts an actual code to a representation with unambiguous characters""" + code_first = code[0] + code = code.upper() + if code_first != 'A': + code = code.replace('A', 'V') # A might be confused with 4 + code = code.replace('E', 'W') # E might be confused with 3 + code = code.replace('I', 'X') # I might be confused with 1 + code = code.replace('O', 'Y') # O might be confused with 0 + code = code_first + code[1:] + return code + +def generate_checksum(base_code, base = 30): + if len(base_code) != 15: + raise RuntimeError("Bad argument to checksum") + # https://www.reddit.com/r/SwitchHacks/comments/f7psrk/anatomy_of_an_eshop_card_code/fijt5d4/ + cksum = sum(int(c, base) for c in base_code) % base + return CHARSET[cksum] + +def validate(code): + if len(code) != 16: return False + # Make uppercase if necessary, then change X and Y back to their base 33 equivalents + unmangled_code = safe2real(code) + theirs = unmangled_code[15] + code_type = get_type(code) + base = 33 if code_type == '_A_' else 30 + mine = generate_checksum(unmangled_code[0:15], base) + return theirs == mine + +def parse(code): + serial = None + external_validator = None + code_type = get_type(code) + unmangled_code = safe2real(code) + if code_type == 'NUM': + serial = int(code[0:8]) + external_validator = int(code[8:]) + elif code_type == '_A_': + serial = int(unmangled_code[1:8], 33) + external_validator = int(unmangled_code[8:15], 33) + elif code_type == '_B_' or code_type == '_C_': + serial = int(unmangled_code[1:8], 30) + external_validator = int(unmangled_code[8:15], 30) + else: + print(" unknown card generation '%s'" % code[0]) + return + + print(" ser#: {:010d} / Ext. validator: {:010d}".format(serial, external_validator)) + +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'