Move methods to library file. Used for brute-a.py for now.
This commit is contained in:
parent
9cd21013e5
commit
cc7ca00256
102
brute-a.py
102
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))
|
||||
|
78
ninty/codes.py
Executable file
78
ninty/codes.py
Executable file
@ -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'
|
Loading…
x
Reference in New Issue
Block a user