diff --git a/tools/lastpass2hashcat.py b/tools/lastpass2hashcat.py new file mode 100755 index 000000000..d4fc095a9 --- /dev/null +++ b/tools/lastpass2hashcat.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Author: hansvh <6390369+hans-vh@users.noreply.github.com> +# Version: 0.0.6 +# License: MIT + +""" +Files can be found here: +Android: /data/data/com.lastpass.lpandroid/files +Others: See https://support.lastpass.com/help/where-is-my-lastpass-data-stored-on-my-computer-lp070008 + +Tested OK with: +- LastPass for Android (com.lastpass.lpandroid) v5.12.0.10004 +- LastPass for Chrome v4.101.1 +- LastPass for Opera v4.101.1 +- LastPass for Firefox v4.101.0 +""" + +import sys +import os +import sqlite3 +from base64 import b64decode +from re import search + + +def parse_encu(data): + """Parse ENCU and return IV and AES-256-CBC encrypted email to compare against""" + data = data.decode("utf-8") + + initialization_vector = None + encrypted_email = None + + try: + # Format: ![B64]|[B64] + result = search(r"^!(.*)\|(.*)$", data) + initialization_vector = result.group(1) + encrypted_email = result.group(2) + + initialization_vector = b64decode(initialization_vector).hex() + encrypted_email = b64decode(encrypted_email).hex() + except: + # B64 Only. This implies EBC, not CBC, mode and IV is found elsewhere, e.g., in database + encrypted_email = b64decode(data).hex() + + return initialization_vector, encrypted_email + + +def open_file(file_name): + """Open file and return contents""" + with open(file_name, "rb") as file_handle: + return file_handle.read() + + +def parse_vault(xml): + """Parse Vault according to format: 4 bytes ASCII identifier, 4 bytes size, size bytes data""" + magic_bytes = xml[:4].decode("utf-8") + if magic_bytes != "LPAV": + sys.exit(f"Expected LPAV in base 64 decoded XML, but found {magic_bytes}") + + offset = 0 + while offset < len(xml): + identifier = xml[offset:offset + 4].decode("utf-8") + offset = offset + 4 + size = int.from_bytes(xml[offset:offset + 4], byteorder='big') + offset = offset + 4 + data = xml[offset:offset + size] + + if identifier == 'ENCU': + initialization_vector, encrypted_email = parse_encu(data) + return initialization_vector, encrypted_email + + offset = offset + size + + return None, None + + +def sqlite_parse_chromium(cur): + """Chrome and Opera""" + iterations = -1 + xml = "" + try: + res = cur.execute("SELECT data FROM LastPassData WHERE type='accts'") + (xml,) = res.fetchone() + result = search(r"^iterations=(\d+);(.*)$", xml) + iterations = result.group(1) + xml = result.group(2) + xml = b64decode(xml) + except: + return None, None + + return iterations, xml + + +def sqlite_parse_firefox(cur): + """Firefox""" + iterations = -1 + encu = "" + try: + res = cur.execute("SELECT value FROM data WHERE key LIKE '%sch'") + encu, = res.fetchone() + encu = encu.decode("utf-8") + encu = encu[encu.find("!"):] + encu = encu[:encu.find("\n")] + encu = bytes(encu, "utf-8") + res = cur.execute("SELECT value FROM data WHERE key LIKE '%key_iter'") + iterations, = res.fetchone() + iterations = int(iterations) + except: + return None, None + + return iterations, encu + + +def main(): + """Entry point""" + if len(sys.argv) < 3: + sys.exit(f"Usage: {sys.argv[0]} ") + + file_name = sys.argv[1] + if not os.path.exists(file_name): + sys.exit(f"File {file_name} does not exist") + + file_content = open_file(file_name) + magic_bytes = file_content[:5].decode("utf-8") + + # Output will contain the following fields (in order), colon separated + encrypted_email = "" + iterations = -1 + email = sys.argv[2].lower() + initialization_vector = "" + + if magic_bytes == "LPB64": + # Android App + iterations = 100100 + xml = b64decode(file_content[5:]) + initialization_vector, encrypted_email = parse_vault(xml) + + elif magic_bytes == "SQLit": + # Browser Extension + con = sqlite3.connect(file_name) + cur = con.cursor() + + # First try Chromium based browsers + iterations, xml = sqlite_parse_chromium(cur) + if iterations and xml: + initialization_vector, encrypted_email = parse_vault(xml) + + # Then try Firefox + if not encrypted_email or not iterations or not initialization_vector: + iterations, encu = sqlite_parse_firefox(cur) + initialization_vector, encrypted_email = parse_encu(encu) + + # Finally give up + if not encrypted_email or not iterations or not initialization_vector: + sys.exit("Unexpected behaviour in SQLite database parsing") + + con.close() + else: + sys.exit(f"Expected LPB64 or SQLit in file, but found {magic_bytes}") + + print(f"{encrypted_email}:{iterations}:{email}:{initialization_vector}") + + +if __name__ == "__main__": + main()