ninty-codes/ninty-codes.py

197 lines
6.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',
'B14BNFY60LBD7KTG',
'B14BNFY72J7S1T57',
'B14BNFY8588M6FS9',
'B14BNFY9464NHGPD',
'B14BNFYV3F8WCLTL',
'B14BNFYB3YDKY4Q4',
'B14CCRDB5P74VCNR',
'B14CCRDC0J4BRJ7T',
'B14CCRDD4X8L97BL',
'B14CCRDW2496B1MT',
'B14CCRDF30RTDK3V',
'B14CCRDG14TH9VG2',
'B14CCRDH382RJPPQ',
'B14CP5QN168XKTXR',
'B14CP5QY1236N081',
'B14CP5QP5CB1XY44',
'B14CP5QQ311FQ5FQ',
'B14CP5QR4Y7BG4QN',
'B14CP5QS1X46M2L6',
'B14CP5QT2S17K53T',
'B14D8WQ610Q2S26S',
'B14D8WQ7113VV6V4',
'B14D8WQ834BFNP0G',
'B14D8WQV3Q41N3KH',
'B14D8WQB45PJ1M7L',
'B14DGMW12MFCV33T', # https://www.reddit.com/r/Splatoon_2/comments/ee5mhm/splatoon_2_jersey_and_shoes_codes_up_for_grabs/
'C00V1HK6KC0VMDLW', # https://www.reddit.com/r/splatoon/comments/ejepc2/hi_all_just_signed_up_for_the_nintendo_family/
'C00V1HK7N1WF5SYR',
'C00V1HK8PF42N06N',
'C00V1HK90VSR0PGP',
'C00V1HKBL941HH9T',
'C00V1HKCJFNTF1QK',
'C00V1HKDRW7VD14T',
'C00V1HKFB2XP76V4',
'C01DHQVVS3THVW6G',
'C01WB9K31B1H3K03',
'C01WRRQKCJ6YXFM3',
'C049GTST9QGLPCSY',
'C049GTT0V2K86RKC', # https://www.reddit.com/r/splatoon/comments/ecc1u4/i_dont_play_splatoon_so_i_thought_i_would_gift/
'C049GTT1QTLK1H26',
'C049GTT277GG78L3',
'C049GTT302810G4D',
'C049GTT44SLTW4HV',
'C049GTT55RYWV004',
'C049GTT6V4S1V5M5',
'C049K4B12TSQB8L6', # https://www.reddit.com/r/Splatoon_2/comments/ee5mhm/splatoon_2_jersey_and_shoes_codes_up_for_grabs/
'C049K4B2XCRM6RD7',
'C049K4B3S5YSCP49',
'C049K4B4JK5LB6LH',
'C049K4B51R1V17KC',
'C049K4B6V37X301X',
'C049K4B7PPP386JS',
'C049MJJDSJHYT4WN',
'C049MJJWDCMSF30C',
'C049MJJFG50675D2',
'C049MJJG0PFXR3HQ',
'C049MJJHT6RTH6CX',
'C049MJJX1YBTY02W',
'C049MJJJW85MWP2W',
'C04BK55WHPWT517J',
'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, 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 = generateChecksum(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'
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 != 'NUM':
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()