""" amiiboconvert.py 3/12/2022 Modified Amiibo Flipper Conversion Code Original Code by Friendartiste Modified by Lamp Modified again by VapidAnt Modified and commented by bjschafer Execute with python amiiboconvert -h to see options """ import argparse import logging import os import pathlib from typing import Tuple def write_output(name: str, assemble: str, out_dir: str): """ Handles writing the converted file :param name: The base filename - e.g. for Foo.bin, Foo :param assemble: The converted flipper-compatible contents :param out_dir: The directory to place Foo.nfc in """ with open(os.path.join(out_dir, f"{name}.nfc"), "wt") as f: f.write(assemble) def convert(contents: bytes) -> Tuple[str, int]: """ Convert from bytes into the Page-based format expected by flipper Each "Page" is 4 bytes hex, notated like: Page 0: DE AD BE EF To process, we grab one byte at a time, turn it into a hex string, and store it in `page`. When page is "full" (has 4 bytes in it), we flush it to the buffer. When all's said and done, buffer contains text ready for writing to the end of a .nfc file. Also tracks and returns running page number, since that's also needed. :param contents: byte array we're reading, from a .bin file :return: The full string of Pages, suitable for writing to a file """ buffer = [] page_count = 0 page = [] for i in range(len(contents) - 1): byte = contents[i : i + 1].hex() page.append(byte) if len(page) == 4: buffer.append(f"Page {page_count}: {' '.join(page).upper()}") page = [] page_count += 1 # we may have an unfilled page. This needs to be filled out and appended logging.debug(f"We have an unfilled final page: {page} with length {len(page)}") if len(page) > 0: # pad with zeroes for i in range(len(page) - 1, 3): page.append("00") buffer.append(f"Page {page_count}: {' '.join(page).upper()}") page_count += 1 return "\n".join(buffer), page_count def get_uid(contents: bytes) -> str: """ the UID appears to be made up of the first 3 bytes, a byte is skipped, and then the next 4 bytes :param contents: The bytes object we're operating on :return: something like `23 20 41 6D 69 69 62 6F` """ page = [] for i in range(3): byte = contents[i : i + 1].hex() page.append(byte) for i in range(4, 8): byte = contents[i : i + 1].hex() page.append(byte) return " ".join(page).upper() def assemble_code(contents: {hex}) -> str: """ Convert from .bin files to Flipper text-like .nfc files :param contents: File contents upon which .hex() can be called :return: A string to be written to a file """ conversion, page_count = convert(contents) return f"""Filetype: Flipper NFC device Version: 2 # Nfc device type can be UID, Mifare Ultralight, Bank card Device type: NTAG215 # UID, ATQA and SAK are common for all formats UID: {get_uid(contents)} ATQA: 44 00 SAK: 00 # Mifare Ultralight specific data Signature: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Mifare version: 00 04 04 02 01 00 11 03 Counter 0: 0 Tearing 0: 00 Counter 1: 0 Tearing 1: 00 Counter 2: 0 Tearing 2: 00 Pages total: {page_count} {conversion} """ def convert_file(input_path: str, output_path: str): """ Handles reading, converting, and writing a single file :param input_path: The full path to the .bin file :param output_path: The base directory to output to """ input_extension = os.path.splitext(input_path)[1] if input_extension == ".bin": logging.info(f"Writing: {input_path}") with open(input_path, "rb") as file: contents = file.read() name = os.path.split(input_path)[1] write_output(name.split(".bin")[0], assemble_code(contents), output_path) elif input_extension == ".nfc": logging.warning(f"Seems like {input_path} may already be Flipper-compatible!") else: logging.info(f"{input_path} doesn't seem like a relevant file, skipping") def process(path: str, output_path: str): """ Process an input file, or walk through an input directory and process every matching .bin file therein :param path: Path to a single file or a directory containing one or more .bin files :param output_path: The base directory to output to """ if os.path.isfile(path): convert_file(path, output_path) else: for filename in os.listdir(path): new_path = os.path.join(path, filename) logging.debug(f"Current file: {filename}; Current path: {new_path}") if os.path.isfile(path): convert_file(path, output_path) else: logging.debug(f"Recursing into: {new_path}") process(new_path, output_path) def get_args(): parser = argparse.ArgumentParser() parser.add_argument( "-i", "--input-path", required=True, type=pathlib.Path, help="Single file or directory tree to convert", ) parser.add_argument( "-o", "--output-path", required=False, type=pathlib.Path, help="Directory to store output in. Will be created if it doesn't exist. If not specified, the output will be " "stored in the same location as the original, with a '.nfc' extension.", ) parser.add_argument( "-v", "--verbose", action="count", default=0, help="Show extra info: pass -v to see what's going on, pass -vv to get useful debug info", ) args = parser.parse_args() if args.verbose >= 2: # set debug logging.basicConfig(level=logging.DEBUG) elif args.verbose >= 1: # set info logging.basicConfig(level=logging.INFO) logging.debug(f"Parsed args into {args}") return args def main(): args = get_args() # single file mode if os.path.isfile(args.input_path): if not args.output_path: args.output_path = os.path.split(args.input_path)[0] # recursive directory mode elif os.path.isdir(args.input_path): if not args.output_path: logging.exception( ValueError( f"{args.input_path} is a directory, but no output path given." ) ) elif not os.path.exists(args.input_path): logging.exception( FileNotFoundError(f"{args.input_path} doesn't actually exist") ) logging.debug(f"Going to create output directory {args.output_path}") os.makedirs(args.output_path, exist_ok=True) logging.debug(f"input: {args.input_path}, output: {args.output_path}") process(args.input_path, args.output_path) if __name__ == "__main__": main() print("----Good Execution----")