File: bitwarden2hashcat.py

package info (click to toggle)
hashcat 6.2.6%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 63,932 kB
  • sloc: lisp: 584,043; ansic: 372,246; perl: 24,890; cpp: 23,731; sh: 3,927; python: 868; makefile: 777
file content (162 lines) | stat: -rwxr-xr-x 4,918 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python3
"""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("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)