#!/usr/pkg/bin/python3.13

#
# Author......: See docs/credits.txt
# License.....: MIT
#

import binascii
import struct
from argparse import ArgumentParser
from base64 import b64decode
from collections import namedtuple
from struct import Struct
from sys import stderr
from xml.dom import minidom

SIGNATURE = "$vbox$0$"

KEY_STORE_PROPERTY_NAME = "CRYPT/KeyStore"

KEY_STORE_STRUCT_FMT = "<4sxb32s32sI32sI32sI32sII64s"
KEY_STORE_STRUCT = Struct(KEY_STORE_STRUCT_FMT)

KeyStore = namedtuple(
    "KeyStore",
    [
        "FileHeader",
        "Version",
        "EVP_Algorithm",
        "PBKDF2_Hash",
        "Key_Length",
        "Final_Hash",
        "KL2_PBKDF2",
        "Salt2_PBKDF2",
        "Iteration2_PBKDF2",
        "Salt1_PBKDF2",
        "Iteration1_PBKDF2",
        "EVP_Length",
        "Enc_Password",
    ],
)


def print_warning(msg):
    print("Warning!", msg + ".", file=stderr)


def print_error(msg):
    print("Error!", msg + "!", file=stderr)
    exit(1)


def process_hard_disk(hard_disk):
    props = hard_disk.getElementsByTagName("Property")
    props = filter(lambda prop: prop.getAttribute("name") == KEY_STORE_PROPERTY_NAME, props)
    try:
        prop = next(props)  # assuming there is only one key store property per hard disk
        key_store = process_property(prop)
    except StopIteration:
        return None
    return key_store


def process_property(property):
    if not property.hasAttribute("value"):
        raise RuntimeWarning("Malformed key store property")
    key_store = property.getAttribute("value")
    try:
        key_store = b64decode(key_store)
        key_store = KEY_STORE_STRUCT.unpack(key_store)
        key_store = KeyStore(*key_store)
        int(key_store.Key_Length)
        return key_store
    except binascii.Error as error:
        raise RuntimeError("Malformed Base64 payload in key store property") from error
    except (ValueError, struct.error) as error:
        raise RuntimeError("Malformed payload in key store property") from error


if __name__ == "__main__":
    parser = ArgumentParser(description="virtualbox2hashcat extraction tool")
    parser.add_argument("path", type=str, help="path to VirtualBox file")

    args = parser.parse_args()

    try:
        document = minidom.parse(args.path)
    except IOError as error:
        print_error("Cannot read a file: " + error.strerror)

    hds = document.getElementsByTagName("HardDisk")
    if len(hds) == 0:
        print_error("No configured hard drives detected!")

    key_stores = []
    for hd in hds:
        try:
            key_store = process_hard_disk(hd)
            if key_store is not None:
                key_stores.append(key_store)
        except RuntimeWarning as warning:
            print_warning(warning)
        except RuntimeError as error:
            print_error(error)
    if len(key_stores) == 0:
        print_error("No valid key store found")
    for key_store in key_stores:
        key_length = int(key_store.Key_Length)
        hash = (
            SIGNATURE
            + str(key_store.Iteration1_PBKDF2)
            + "$"
            + key_store.Salt1_PBKDF2.hex()
            + "$"
            + str(key_length // 4)  # key_length in bits divided by sizeof(u32) to get the length in 32-bit words
            + "$"
            + key_store.Enc_Password[:key_length].hex()
            + "$"
            + str(key_store.Iteration2_PBKDF2)
            + "$"
            + key_store.Salt2_PBKDF2.hex()
            + "$"
            + key_store.Final_Hash.rstrip(b"\x00").hex()
        )
        print(hash)
