-
-
Save anonymous/0a3e16f8f814deb2a056 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
# Use python2 since nfcpy doesn't support python3 | |
import argparse | |
import binascii | |
import collections | |
import struct | |
import sys | |
# libusb: http://libusb.info/ | |
# pyusb: https://walac.github.io/pyusb/ | |
# nfcpy: https://nfcpy.readthedocs.org/ | |
import nfc | |
# PyCryptodome: http://pycryptodome.readthedocs.org/ | |
from Crypto.Cipher import AES | |
from Crypto.Hash import HMAC | |
from Crypto.Hash import SHA256 | |
from Crypto.Util import Counter | |
DUMP_SIZE = 520 | |
PAGE_SIZE = 4 | |
CT = 0x88 | |
PACK = b'\x80\x80' | |
CC = b'\xF1\x10\xFF\xEE' | |
STATIC_LOCK = b'\x00\x00\x0F\xE0' | |
DYNAMIC_LOCK = b'\x01\x00\x0F\x00' | |
CFG0 = b'\x00\x00\x00\x04' | |
CFG1 = b'\x5F\x00\x00\x00' | |
C_PWD_AUTH = b'\x1B' | |
MasterKeys = collections.namedtuple('MasterKeys', | |
('hmac_key', 'type_string', 'magic', 'xor_pad')) | |
DerivedKeys = collections.namedtuple('DerivedKeys', | |
('aes_key', 'aes_iv', 'hmac_key')) | |
class FileType(argparse.FileType): | |
"""Fix for binary stdin/stdout on Windows.""" | |
def __call__(self, string): | |
f = super(FileType, self).__call__(string) | |
if string == '-' and 'b' in self._mode and sys.platform == 'win32': | |
import os, msvcrt | |
msvcrt.setmode(f.fileno(), os.O_BINARY) | |
return f | |
class Drbg(object): | |
def __init__(self, hmac_key, seed): | |
self._hmac_key = hmac_key | |
self._seed = bytes(seed) | |
self._iteration = 0 | |
self._cache = None | |
self._pos = 0 | |
def _update(self): | |
hmac = HMAC.new(self._hmac_key, digestmod=SHA256) | |
hmac.update(struct.pack('>H', self._iteration)) | |
hmac.update(self._seed) | |
self._iteration += 1 | |
self._cache = hmac.digest() | |
self._pos = 0 | |
def get_bytes(self, length): | |
result = bytearray() | |
while len(result) < length: | |
if self._cache is None or self._pos >= len(self._cache): | |
self._update() | |
l = min(length - len(result), len(self._cache) - self._pos) | |
result.extend(memoryview(self._cache)[self._pos:self._pos + l]) | |
self._pos += l | |
return result | |
def get_ntag215(): | |
clf = nfc.ContactlessFrontend('usb') | |
tag = clf.connect(rdwr={'on-connect': lambda Tag: False}) | |
if not isinstance(tag, nfc.tag.tt2_nxp.NTAG215): | |
raise RuntimeError('Tag is not an NTag215') | |
return tag | |
def get_master_keys(keyfile): | |
hmac_key = keyfile.read(16) | |
type_string = keyfile.read(14) | |
keyfile.read(1) | |
magic_size = ord(keyfile.read(1)) | |
magic = keyfile.read(magic_size) | |
keyfile.read(16 - magic_size) | |
xor_pad = keyfile.read(32) | |
return MasterKeys(hmac_key, type_string, magic, xor_pad) | |
def get_base_seed(data): | |
data = memoryview(data) | |
base_seed = bytearray() | |
base_seed.extend(data[0x11:0x13]) | |
base_seed.extend(b'\0' * 0x0E) | |
for _ in xrange(2): | |
base_seed.extend(data[0x00:0x08]) | |
base_seed.extend(data[0x60:0x80]) | |
return base_seed | |
def get_seed(master_keys, base_seed): | |
base_seed = memoryview(base_seed) | |
seed = bytearray() | |
seed.extend(master_keys.type_string); | |
seed.extend(base_seed[0:16 - len(master_keys.magic)]) | |
seed.extend(master_keys.magic) | |
seed.extend(base_seed[0x10:0x20]) | |
for i in range(0x20): | |
seed.append(ord(base_seed[0x20 + i]) ^ ord(master_keys.xor_pad[i])) | |
return seed | |
def get_derived_keys(master_keys, data): | |
base_seed = get_base_seed(data) | |
seed = get_seed(master_keys, base_seed) | |
drbg = Drbg(master_keys.hmac_key, seed) | |
aes_key = drbg.get_bytes(16) | |
aes_iv = drbg.get_bytes(16) | |
hmac_key = drbg.get_bytes(16) | |
return DerivedKeys(aes_key, aes_iv, hmac_key) | |
def set_uid(data, uid): | |
data[0:3] = uid[0:3] | |
data[3] = CT ^ uid[0] ^ uid[1] ^ uid[2] | |
data[4:8] = uid[3:7] | |
data[8] = uid[3] ^ uid[4] ^ uid[5] ^ uid[6] | |
def get_pwd(uid): | |
return bytearray(( | |
0xAA ^ uid[1] ^ uid[3], | |
0x55 ^ uid[2] ^ uid[4], | |
0xAA ^ uid[3] ^ uid[5], | |
0x55 ^ uid[4] ^ uid[6], | |
)) | |
def is_unlocked(tag): | |
header = bytearray(tag.read(0)) | |
cfg = bytearray(tag.read(130)) | |
return (header[10] == 0 and header[11] == 0 and cfg[0] == 0 and cfg[1] == 0 | |
and cfg[2] == 0 and cfg[7] >= 135 and cfg[8] & 0b01000000 == 0) | |
def authenticate(tag, pwd): | |
"""nfcpy's implementation of authenticate is broken.""" | |
return tag.transceive(C_PWD_AUTH + pwd) == PACK | |
def cipher(mode, aes_key, aes_iv, data): | |
ctr = Counter.new(8 * AES.block_size, | |
initial_value=int(binascii.hexlify(aes_iv), 16)) | |
aes = AES.new(key=bytes(aes_key), mode=AES.MODE_CTR, counter=ctr) | |
f = getattr(aes, mode) | |
data[0x014:0x034] = f(bytes(data[0x014:0x034])) | |
data[0x0A0:0x208] = f(bytes(data[0x0A0:0x208])) | |
def hmac(hmac_key, data): | |
hmac = HMAC.new(bytes(hmac_key), digestmod=SHA256) | |
hmac.update(bytes(data[0x011:0x034])) | |
hmac.update(bytes(data[0x0A0:0x208])) | |
hmac.update(bytes(data[0x034:0x054])) | |
hmac.update(bytes(data[0x000:0x008])) | |
hmac.update(bytes(data[0x054:0x080])) | |
return hmac.digest() | |
def decrypt(master_keys, data): | |
derived_keys = get_derived_keys(master_keys, data) | |
cipher('decrypt', derived_keys.aes_key, derived_keys.aes_iv, data) | |
if not memoryview(data)[0x80:0xA0] == hmac(derived_keys.hmac_key, data): | |
raise RuntimeError('Signature check failed') | |
def scan(master_keys): | |
tag = get_ntag215() | |
data = bytearray() | |
for page in xrange(0, DUMP_SIZE // PAGE_SIZE, 4): | |
data.extend(tag.read(page)) | |
decrypt(master_keys, data) | |
return data | |
def encrypt(master_keys, data): | |
derived_keys = get_derived_keys(master_keys, data) | |
data[0x80:0xA0] = hmac(derived_keys.hmac_key, data) | |
cipher('encrypt', derived_keys.aes_key, derived_keys.aes_iv, data) | |
def restore(master_keys, data): | |
tag = get_ntag215() | |
uid = bytearray(tag.identifier) | |
set_uid(data, uid) | |
pwd = get_pwd(uid) | |
unlocked = is_unlocked(tag) | |
if not unlocked: | |
if not authenticate(tag, pwd): | |
raise RuntimeError('Tag auth failed') | |
locked = bytearray() | |
for page in xrange(0x0D, 0x20, 4): | |
locked.extend(tag.read(page)) | |
data[0x34:0x80] = locked | |
encrypt(master_keys, data) | |
for page in xrange(0x04, 0x0D): | |
tag.write(page, data[page * PAGE_SIZE:(page + 1) * PAGE_SIZE]) | |
if unlocked: | |
for page in xrange(0x0D, 0x20): | |
tag.write(page, data[page * PAGE_SIZE:(page + 1) * PAGE_SIZE]) | |
for page in xrange(0x20, 0x82): | |
tag.write(page, data[page * PAGE_SIZE:(page + 1) * PAGE_SIZE]) | |
if unlocked: | |
tag.write(0x03, CC) | |
tag.write(0x02, STATIC_LOCK) | |
tag.write(0x82, DYNAMIC_LOCK) | |
tag.write(0x85, pwd) | |
tag.write(0x86, PACK + b'\0\0') | |
tag.write(0x83, CFG0) | |
tag.write(0x84, CFG1) | |
if __name__ == '__main__': | |
parent_parser_infile = argparse.ArgumentParser(add_help=False) | |
parent_parser_infile.add_argument('-i', '--infile', default='-', | |
type=FileType('rb'), | |
help='input file; if not specified, stdin will be used') | |
parent_parser_outfile = argparse.ArgumentParser(add_help=False) | |
parent_parser_outfile.add_argument('-o', '--outfile', default='-', | |
type=FileType('wb'), | |
help='output file; if not specified, stdout will be used') | |
parser = argparse.ArgumentParser() | |
parser.add_argument('-k', '--keyfile', required=True, | |
type=FileType('rb'), | |
help='key set file; for retail amiibo, use "retail unfixed" key set') | |
subparsers = parser.add_subparsers(dest='command') | |
parser_scan = subparsers.add_parser('scan', | |
parents=(parent_parser_outfile,), help='scan and decrypt amiibo') | |
parser_scan = subparsers.add_parser('decrypt', | |
parents=(parent_parser_infile, parent_parser_outfile), | |
help='decrypt and check amiibo dump') | |
parser_scan = subparsers.add_parser('encrypt', | |
parents=(parent_parser_infile, parent_parser_outfile), | |
help='encrypt and sign amiibo dump') | |
parser_scan = subparsers.add_parser('restore', | |
parents=(parent_parser_infile,), help='encrypt and restore amiibo') | |
args = parser.parse_args() | |
master_keys = get_master_keys(args.keyfile) | |
if hasattr(args, 'infile'): | |
data = args.infile.read(DUMP_SIZE) | |
data = bytearray(data) | |
if args.command == 'scan': | |
data = scan(master_keys) | |
elif args.command == 'decrypt': | |
decrypt(master_keys, data) | |
elif args.command == 'encrypt': | |
encrypt(master_keys, data) | |
elif args.command == 'restore': | |
restore(master_keys, data) | |
if hasattr(args, 'outfile'): | |
args.outfile.write(data) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment