#!/usr/bin/env python3
"""Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data"""

#
# Script to extract the hash for use with hashcat mode 23400.
#
# Based on bitwarden2john.py https://github.com/willstruggle/john/blob/master/bitwarden2john.py
#
# Various data locations are documented here: https://bitwarden.com/help/data-storage/#on-your-local-machine
#
# Author: https://github.com/Greexter
# License: MIT
#

import os
import argparse
import sys
import base64
import traceback

try:
    import json
    assert json
except ImportError:
    try:
        import simplejson as json
    except ImportError:
        print("Please install json module which is currently not installed.\n", file=sys.stderr)
        sys.exit(-1)


def process_sqlite(path):
    try:
        import snappy
    except ImportError:
        print("Please install python-snappy module.\n", file=sys.stderr)
        sys.exit(-1)
    try:
        import sqlite3
    except ImportError:
        print("Please install sqlite3 module.\n", file=sys.stderr)
        sys.exit(-1)

    conn = sqlite3.connect(path)
    cur = conn.cursor()
    data = cur.execute('SELECT * FROM object_data')
    fetched = data.fetchall()

    # uses undocumented nonstandard data format
    # probably can break in the future
    dataValue = snappy.decompress(fetched[0][4])

    key_hash = dataValue.split(b"keyHash")[1][9:53].decode()
    email = dataValue.split(b"email")[1][11:].split(b'\x00')[0].decode()
    iterations = int.from_bytes(dataValue.split(b"kdfIterations")[1][3:7], byteorder="little")

    return [(email, key_hash, iterations)]


def process_leveldb(path):
    try:
        import leveldb
    except ImportError:
        print("Please install the leveldb module for full functionality!\n", file=sys.stderr)
        sys.exit(-1)

    db = leveldb.LevelDB(path, create_if_missing=False)

    try:
        out = []
        accIds = db.Get(b'authenticatedAccounts')
        accIds = json.loads(accIds)

        for id in accIds:
            authAccData = db.Get(id.strip('"').encode())
            out.append(extract_json_profile(json.loads(authAccData)))

        return out
    except(KeyError):
        # support for older Bitwarden versions (before account switch implementation)
        # data is stored in different format
        print("Failed to extract data, trying old format.", file=sys.stderr)
        email = db.Get(b'userEmail')\
            .decode('utf-8')\
            .strip('"')
        key_hash = db.Get(b'keyHash')\
            .decode("ascii").strip('"')
        iterations = int(db.Get(b'kdfIterations').decode("ascii"))

    return [(email, key_hash, iterations)]


def process_json(data):
    data = json.loads(data)

    try:
        out = []
        accIds = data["authenticatedAccounts"]
        for id in accIds:
            authAccData = data[id.strip('"')]
            out.append(extract_json_profile(authAccData))

        return out
    except(KeyError):
        print("Failed to extract data, trying old format.", file=sys.stderr)
        email = data["rememberedEmail"]
        hash = data["keyHash"]
        iterations = data["kdfIterations"]

    return [(email, hash, iterations)]


def extract_json_profile(data):
    profile = data["profile"]
    email = profile["email"]
    iterations = profile["kdfIterations"]
    hash = profile["keyHash"]
    return email, hash, iterations


def process_file(filename, legacy = False):
    try:
        if os.path.isdir(filename):
            # Chromium based
            data = process_leveldb(filename)
        elif filename.endswith(".sqlite"):
            # Firefox
            data = process_sqlite(filename)
        elif filename.endswith(".json"):
            # json - Desktop
            with open(filename, "rb") as f:
                data = f.read()
                data = process_json(data)
        else:
            print("Unknown storage. Don't know how to extract data.", file=sys.stderr)
            sys.exit(-1)

    except (ValueError, KeyError):
        traceback.print_exc()
        print("Missing values, user is probably logged out.", file=sys.stderr)
        return
    except:
        traceback.print_exc()
        return

    iterations2 = 1 if legacy else 2
    for entry in data:
        if len(entry) != 3:
            print("[error] %s could not be parsed properly!\nUser is probably logged out." % filename, file=sys.stderr)
            continue

        print("$bitwarden$2*%d*%d*%s*%s" %
            (entry[2], iterations2, base64.b64encode(entry[0].encode("ascii")).decode("ascii"), entry[1]))


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("paths", type=str, nargs="+")
    parser.add_argument("--legacy", action="store_true", help="Used for older versions of Bitwarden (before static iteration count had been changed).")

    args = parser.parse_args()

    for p in args.paths:
        process_file(p, args.legacy)
