diff --git a/tools/bitwarden2hashcat.py b/tools/bitwarden2hashcat.py new file mode 100644 index 000000000..4afa62d20 --- /dev/null +++ b/tools/bitwarden2hashcat.py @@ -0,0 +1,143 @@ +"""Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data""" + +# +# 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("[WARNING] Please install the leveldb module for full functionality!\n", file=sys.stderr) + return + + db = leveldb.LevelDB(path, create_if_missing=False) + + try: + active = db.Get(b'activeUserId').decode().strip('"') + data = db.Get(active.encode()) + return process_json(data) + except(KeyError): + # support for older Bitwarden versions (before account switch implementation) + # data is stored in different format + print("Failed to exctract data, trying old format.", file=sys.stderr) + email = db.Get(b'userEmail')\ + .decode("utf-8")\ + .strip('"').rstrip('"') + key_hash = db.Get(b'keyHash')\ + .decode("ascii").strip('"').rstrip('"') + iterations = int(db.Get(b'kdfIterations').decode("ascii")) + + return email, key_hash, iterations + + +def process_json(data): + data = json.loads(data) + try: + profile = data["profile"] + email = profile["email"] + iterations = profile["kdfIterations"] + hash = profile["keyHash"] + except(KeyError): + print("Failed to exctract data, trying old format.", file=sys.stderr) + email = data["rememberedEmail"] + hash = data["keyHash"] + iterations = data["kdfIterations"] + + return email, hash, iterations + + +def process_file(filename, legacy = False): + try: + if os.path.isdir(filename): + # Chromium based + email, key_hash, iterations = process_leveldb(filename) + elif filename.endswith(".sqlite"): + # Firefox + email, key_hash, iterations = process_sqlite(filename) + elif filename.endswith(".json"): + # json - Desktop + with open(filename, "rb") as f: + data = f.read() + email, key_hash, iterations = process_leveldb(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 + + if not email or not key_hash or not iterations: + print("[error] %s could not be parsed properly!\nUser is probably logged out." % filename, file=sys.stderr) + sys.exit(-1) + + iterations2 = 1 if legacy else 2 + + print(f"$bitwarden$2*%d*%d*%s*%s\n" % + (iterations, iterations2, base64.b64encode(email.encode("ascii")).decode("ascii"), key_hash)) + + +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)