diff --git a/tools/bitwarden2hashcat.py b/tools/bitwarden2hashcat.py index 4afa62d20..07b56cb83 100644 --- a/tools/bitwarden2hashcat.py +++ b/tools/bitwarden2hashcat.py @@ -50,50 +50,68 @@ def process_sqlite(path): 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 + + 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 + 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: - active = db.Get(b'activeUserId').decode().strip('"') - data = db.Get(active.encode()) - return process_json(data) + 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 exctract data, trying old format.", file=sys.stderr) + print("Failed to extract data, trying old format.", file=sys.stderr) email = db.Get(b'userEmail')\ - .decode("utf-8")\ - .strip('"').rstrip('"') + .decode('utf-8')\ + .strip('"') key_hash = db.Get(b'keyHash')\ - .decode("ascii").strip('"').rstrip('"') + .decode("ascii").strip('"') iterations = int(db.Get(b'kdfIterations').decode("ascii")) - return email, key_hash, iterations + 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"] + 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 exctract data, trying old format.", file=sys.stderr) + 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 @@ -101,15 +119,15 @@ def process_file(filename, legacy = False): try: if os.path.isdir(filename): # Chromium based - email, key_hash, iterations = process_leveldb(filename) + data = process_leveldb(filename) elif filename.endswith(".sqlite"): # Firefox - email, key_hash, iterations = process_sqlite(filename) + data = 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) + data = process_json(data) else: print("Unknown storage. Don't know how to extract data.", file=sys.stderr) sys.exit(-1) @@ -122,14 +140,14 @@ def process_file(filename, legacy = False): 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 + 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(f"$bitwarden$2*%d*%d*%s*%s\n" % - (iterations, iterations2, base64.b64encode(email.encode("ascii")).decode("ascii"), key_hash)) + print("$bitwarden$2*%d*%d*%s*%s" % + (entry[2], iterations2, base64.b64encode(entry[0].encode("ascii")).decode("ascii"), entry[1])) if __name__ == "__main__":