166 lines
5.1 KiB
Python
Executable File
166 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from functools import reduce
|
|
|
|
# https://www.reddit.com/r/SwitchHacks/comments/f7psrk/anatomy_of_an_eshop_card_code/
|
|
|
|
# taken from the interwebs, none of these are new
|
|
EXAMPLE_CODES = [
|
|
'4237082951213129', # https://cr-cs.tumblr.com/post/153860583013/3ds-store-3ds-redeem-codes
|
|
'5753437561933377',
|
|
'5753437675070492',
|
|
|
|
'A0387RMN33YJNGMC', # https://cr-cs.tumblr.com/post/155573566497/cheap-eshop-codes-3ds-prepaid-card-code-generator-online
|
|
'A039XJDX0WMEHG6W',
|
|
'A03RL0KB2XHHRRBD',
|
|
'A03RL0KC22JL2MEA',
|
|
'A03RL0KD0UXF49FK',
|
|
'A03RL0KE1X8GGMS6',
|
|
'A05A259G2H2PJL5G',
|
|
'A0693YFS0BPM6K2G',
|
|
# 'A071WNTC1BPR5EVU', # https://www.nintendolife.com/news/2015/01/rumour_code_name_steam_demo_to_be_distributed_via_gamestop_stores
|
|
'A0775H3G1MYCJW1Q', # https://www.youtube.com/watch?v=RMsuHPbI04Y
|
|
'A0796BH71Q1DSSYN',
|
|
'A07K7E3702LYF1NM', # https://www.youtube.com/watch?v=tIEnTXxSM98
|
|
|
|
'B0GLFY1H1QR5GGDT',
|
|
'B0K777732YWS7XVF',
|
|
'B0QFT5QX0F2XMF3Q',
|
|
'B0S0W8RT3QBSRHK9',
|
|
'B0TTY433034MJ8FY',
|
|
'B14WC53G32Y9VWVX',
|
|
'B14CP5QN168XKTXR',
|
|
'B14CP5QY1236N081',
|
|
'B14CP5QP5CB1XY44',
|
|
'B14CP5QQ311FQ5FQ',
|
|
'B14CP5QR4Y7BG4QN',
|
|
'B14CP5QS1X46M2L6',
|
|
'B14CP5QT2S17K53T',
|
|
'B14D8WQ610Q2S26S',
|
|
'B14D8WQ7113VV6V4',
|
|
'B14D8WQ834BFNP0G',
|
|
'B14D8WQV3Q41N3KH',
|
|
'B14D8WQB45PJ1M7L',
|
|
|
|
'C00V1HK6KC0VMDLW',
|
|
'C00V1HK7N1WF5SYR',
|
|
'C00V1HK8PF42N06N',
|
|
'C00V1HK90VSR0PGP',
|
|
'C00V1HKBL941HH9T',
|
|
'C00V1HKCJFNTF1QK',
|
|
'C00V1HKDRW7VD14T',
|
|
'C00V1HKFB2XP76V4',
|
|
'C048GTST9QGLPCSY',
|
|
'C049GTT0V2K86RKC',
|
|
'C049GTT1QTLK1H26',
|
|
'C049GTT277GG78L3',
|
|
'C049GTT302810G4D',
|
|
'C049GTT44SLTW4HV',
|
|
'C049GTT55RYWV004',
|
|
'C049GTT6V4S1V5M5',
|
|
'C01DHQVVS3THVW6G',
|
|
'C01WB9K31B1H3K03',
|
|
'C01WRRQKCJ6YXFM3',
|
|
'C080M7Y73KC7B37N',
|
|
'C080M7Y8JGY35R0P',
|
|
'C080M7Y9YWPX531M',
|
|
'C080M7YV4MM8XG81',
|
|
'C080M7YB7M3V2XRN',
|
|
'C080M7YCDBS7Y13M',
|
|
'C080M7YD74CPT83Y',
|
|
'C080M7YW8XLRMTXK',
|
|
]
|
|
|
|
CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
|
|
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
|
|
# Make uppercase if necessary, then change X and Y back to their base 33 equivalents
|
|
unmangled_code = safe2real(code)
|
|
theirs = unmangled_code[15]
|
|
mine = generateChecksum(unmangled_code[0:15])
|
|
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'
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
args = None
|
|
if len(sys.argv) == 1:
|
|
args = EXAMPLE_CODES
|
|
else:
|
|
args = sys.argv[1:]
|
|
seen_chars = {}
|
|
for s in args:
|
|
code_type = get_type(s)
|
|
if not code_type in seen_chars:
|
|
seen_chars[code_type] = set()
|
|
for c in s[1:]:
|
|
seen_chars[code_type].add(c)
|
|
if code_type == '_A_':
|
|
valid = validate(s)
|
|
print('{} {}'.format(s, "☑ " if valid else "☒ "), end='')
|
|
if not valid:
|
|
print()
|
|
continue
|
|
parse(s)
|
|
else:
|
|
print('{} ??'.format(s), end='')
|
|
parse(s)
|
|
for ct, cc in seen_chars.items():
|
|
cc = sorted(list(cc))
|
|
print('Found characters in type {} codes: '.format(ct), end='')
|
|
for c in CHARSET:
|
|
if c in cc:
|
|
print(c, end='')
|
|
else:
|
|
print('-', end='')
|
|
print()
|