#!/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