From: Nicolas Boisselier Date: Fri, 6 Sep 2019 23:33:19 +0000 (+0100) Subject: /opt/nb/bin/firefox_decrypt.py X-Git-Url: https://git.nbdom.net/?a=commitdiff_plain;h=118772cf500293ad1793a967951acf3959565976;p=nb.git /opt/nb/bin/firefox_decrypt.py --- diff --git a/bin/firefox_decrypt.py b/bin/firefox_decrypt.py deleted file mode 100755 index c86afe89..00000000 --- a/bin/firefox_decrypt.py +++ /dev/null @@ -1,1015 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Based on original work from: www.dumpzilla.org - -import argparse -import csv -import ctypes as ct -import json -import logging -import os -import select -import sqlite3 -import sys -from base64 import b64decode -from getpass import getpass -from subprocess import PIPE, Popen - -try: - # Python 3 - from subprocess import DEVNULL -except ImportError: - # Python 2 - DEVNULL = open(os.devnull, 'w') - -try: - # Python 3 - from urllib.parse import urlparse -except ImportError: - # Python 2 - from urlparse import urlparse - -try: - # Python 3 - from configparser import ConfigParser - raw_input = input -except ImportError: - # Python 2 - from ConfigParser import ConfigParser - -PY3 = sys.version_info.major > 2 -LOG = None -VERBOSE = False -SYS64 = sys.maxsize > 2**32 - -if not PY3 and os.name == "nt": - sys.stderr.write("WARNING: You are using Python 2 on Windows. If your " - "passwords include non-alphanumeric characters you " - "will run into problems.\n") - sys.stderr.write("WARNING: Python 2 + Windows is no longer supported. " - "Please use Python 3 instead\n") - -# Windows uses a mixture of different codecs for different components -# ANSI CP1252 for system messages, while NSS uses UTF-8 -# To further complicate things, with python 2.7 the default stdout/stdin codec -# isn't UTF-8 but language dependent (tested on Windows 7) - -if os.name == "nt": - SYS_ENCODING = "cp1252" - LIB_ENCODING = "utf8" -else: - SYS_ENCODING = "utf8" - LIB_ENCODING = "utf8" - -# When using pipes stdin/stdout encoding may be None -USR_ENCODING = sys.stdin.encoding or sys.stdout.encoding or "utf8" - - -def py2_decode(_bytes, encoding=USR_ENCODING): - if PY3: - return _bytes - else: - return _bytes.decode(encoding) - - -def py2_encode(_unicode, encoding=USR_ENCODING): - if PY3: - return _unicode - else: - return _unicode.encode(encoding) - - -def type_decode(encoding): - return lambda x: py2_decode(x, encoding) - - -def get_version(): - """Obtain version information from git if available otherwise use - the internal version number - """ - def internal_version(): - return '.'.join(map(str, __version_info__[:3])) + ''.join(__version_info__[3:]) - - try: - p = Popen(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL) - except OSError: - return internal_version() - - stdout, stderr = p.communicate() - - if p.returncode: - return internal_version() - else: - # Both py2 and py3 return bytes here - return stdout.decode(USR_ENCODING).strip() - - -__version_info__ = (0, 8, 0, "+git") -__version__ = get_version() - - -class NotFoundError(Exception): - """Exception to handle situations where a credentials file is not found - """ - pass - - -class Exit(Exception): - """Exception to allow a clean exit from any point in execution - """ - ERROR = 1 - MISSING_PROFILEINI = 2 - MISSING_SECRETS = 3 - BAD_PROFILEINI = 4 - LOCATION_NO_DIRECTORY = 5 - BAD_SECRETS = 6 - - FAIL_LOCATE_NSS = 10 - FAIL_LOAD_NSS = 11 - FAIL_INIT_NSS = 12 - FAIL_NSS_KEYSLOT = 13 - FAIL_SHUTDOWN_NSS = 14 - BAD_MASTER_PASSWORD = 15 - NEED_MASTER_PASSWORD = 16 - - PASSSTORE_NOT_INIT = 20 - PASSSTORE_MISSING = 21 - PASSSTORE_ERROR = 22 - - READ_GOT_EOF = 30 - MISSING_CHOICE = 31 - NO_SUCH_PROFILE = 32 - - UNKNOWN_ERROR = 100 - KEYBOARD_INTERRUPT = 102 - - def __init__(self, exitcode): - self.exitcode = exitcode - - def __unicode__(self): - return "Premature program exit with exit code {0}".format(self.exitcode) - - -class Credentials(object): - """Base credentials backend manager - """ - def __init__(self, db): - self.db = db - - LOG.debug("Database location: %s", self.db) - if not os.path.isfile(db): - raise NotFoundError("ERROR - {0} database not found\n".format(db)) - - LOG.info("Using %s for credentials.", db) - - def __iter__(self): - pass - - def done(self): - """Override this method if the credentials subclass needs to do any - action after interaction - """ - pass - - -class SqliteCredentials(Credentials): - """SQLite credentials backend manager - """ - def __init__(self, profile): - db = os.path.join(profile, "signons.sqlite") - - super(SqliteCredentials, self).__init__(db) - - self.conn = sqlite3.connect(db) - self.c = self.conn.cursor() - - def __iter__(self): - LOG.debug("Reading password database in SQLite format") - self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType " - "FROM moz_logins") - for i in self.c: - # yields hostname, encryptedUsername, encryptedPassword, encType - yield i - - def done(self): - """Close the sqlite cursor and database connection - """ - super(SqliteCredentials, self).done() - - self.c.close() - self.conn.close() - - -class JsonCredentials(Credentials): - """JSON credentials backend manager - """ - def __init__(self, profile): - db = os.path.join(profile, "logins.json") - - super(JsonCredentials, self).__init__(db) - - def __iter__(self): - with open(self.db) as fh: - LOG.debug("Reading password database in JSON format") - data = json.load(fh) - - try: - logins = data["logins"] - except Exception: - LOG.error("Unrecognized format in {0}".format(self.db)) - raise Exit(Exit.BAD_SECRETS) - - for i in logins: - yield (i["hostname"], i["encryptedUsername"], - i["encryptedPassword"], i["encType"]) - - -class NSSDecoder(object): - class SECItem(ct.Structure): - """struct needed to interact with libnss - """ - _fields_ = [ - ('type', ct.c_uint), - ('data', ct.c_char_p), # actually: unsigned char * - ('len', ct.c_uint), - ] - - class PK11SlotInfo(ct.Structure): - """opaque structure representing a logical PKCS slot - """ - - def __init__(self): - # Locate libnss and try loading it - self.NSS = None - self.load_libnss() - - SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) - SECItemPtr = ct.POINTER(self.SECItem) - - self._set_ctypes(ct.c_int, "NSS_Init", ct.c_char_p) - self._set_ctypes(ct.c_int, "NSS_Shutdown") - self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") - self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) - self._set_ctypes(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, ct.c_char_p) - self._set_ctypes(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p) - self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int) - - # for error handling - self._set_ctypes(ct.c_int, "PORT_GetError") - self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int) - self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32) - - def _set_ctypes(self, restype, name, *argtypes): - """Set input/output types on libnss C functions for automatic type casting - """ - res = getattr(self.NSS, name) - res.restype = restype - res.argtypes = argtypes - setattr(self, "_" + name, res) - - @staticmethod - def find_nss(locations, nssname): - """Locate nss is one of the many possible locations - """ - fail_errors = [] - - for loc in locations: - nsslib = os.path.join(loc, nssname) - LOG.debug("Loading NSS library from %s", nsslib) - - if os.name == "nt": - # On windows in order to find DLLs referenced by nss3.dll - # we need to have those locations on PATH - os.environ["PATH"] = ';'.join([loc, os.environ["PATH"]]) - LOG.debug("PATH is now %s", os.environ["PATH"]) - # However this doesn't seem to work on all setups and needs to be - # set before starting python so as a workaround we chdir to - # Firefox's nss3.dll location - if loc: - if not os.path.isdir(loc): - # No point in trying to load from paths that don't exist - continue - - workdir = os.getcwd() - os.chdir(loc) - - try: - nss = ct.CDLL(nsslib) - except OSError as e: - fail_errors.append((nsslib, str(e))) - else: - LOG.debug("Loaded NSS library from %s", nsslib) - return nss - finally: - if os.name == "nt" and loc: - # Restore workdir changed above - os.chdir(workdir) - - else: - LOG.error("Couldn't find or load '%s'. This library is essential " - "to interact with your Mozilla profile.", nssname) - LOG.error("If you are seeing this error please perform a system-wide " - "search for '%s' and file a bug report indicating any " - "location found. Thanks!", nssname) - LOG.error("Alternatively you can try launching firefox_decrypt " - "from the location where you found '%s'. " - "That is 'cd' or 'chdir' to that location and run " - "firefox_decrypt from there.", nssname) - - LOG.error("Please also include the following on any bug report. " - "Errors seen while searching/loading NSS:") - - for target, error in fail_errors: - LOG.error("Error when loading %s was %s", target, py2_decode(str(error), SYS_ENCODING)) - - raise Exit(Exit.FAIL_LOCATE_NSS) - - def load_libnss(self): - """Load libnss into python using the CDLL interface - """ - if os.name == "nt": - nssname = "nss3.dll" - if SYS64: - locations = ( - "", # Current directory or system lib finder - r"C:\Program Files\Mozilla Firefox", - r"C:\Program Files\Mozilla Thunderbird", - r"C:\Program Files\Nightly", - ) - else: - locations = ( - "", # Current directory or system lib finder - r"C:\Program Files (x86)\Mozilla Firefox", - r"C:\Program Files (x86)\Mozilla Thunderbird", - r"C:\Program Files (x86)\Nightly", - # On windows 32bit these folders can also be 32bit - r"C:\Program Files\Mozilla Firefox", - r"C:\Program Files\Mozilla Thunderbird", - r"C:\Program Files\Nightly", - ) - - # FIXME this was present in the past adding the location where NSS was found to PATH - # I'm not sure why this would be necessary. We don't need to run Firefox... - # TODO Test on a Windows machine and see if this works without the PATH change - # os.environ["PATH"] = ';'.join([os.environ["PATH"], firefox]) - # LOG.debug("PATH is now %s", os.environ["PATH"]) - - elif os.uname()[0] == "Darwin": - nssname = "libnss3.dylib" - locations = ( - "", # Current directory or system lib finder - "/usr/local/lib/nss", - "/usr/local/lib", - "/opt/local/lib/nss", - "/sw/lib/firefox", - "/sw/lib/mozilla", - "/usr/local/opt/nss/lib", # nss installed with Brew on Darwin - "/opt/pkg/lib/nss", # installed via pkgsrc - ) - - else: - nssname = "libnss3.so" - if SYS64: - locations = ( - "", # Current directory or system lib finder - "/usr/lib64", - "/usr/lib64/nss", - "/usr/lib", - "/usr/lib/nss", - "/usr/local/lib", - "/usr/local/lib/nss", - "/opt/local/lib", - "/opt/local/lib/nss", - os.path.expanduser("~/.nix-profile/lib"), - ) - else: - locations = ( - "", # Current directory or system lib finder - "/usr/lib", - "/usr/lib/nss", - "/usr/lib32", - "/usr/lib32/nss", - "/usr/lib64", - "/usr/lib64/nss", - "/usr/local/lib", - "/usr/local/lib/nss", - "/opt/local/lib", - "/opt/local/lib/nss", - os.path.expanduser("~/.nix-profile/lib"), - ) - - # If this succeeds libnss was loaded - self.NSS = self.find_nss(locations, nssname) - - def handle_error(self): - """If an error happens in libnss, handle it and print some debug information - """ - LOG.debug("Error during a call to NSS library, trying to obtain error info") - - code = self._PORT_GetError() - name = self._PR_ErrorToName(code) - name = "NULL" if name is None else name.decode(SYS_ENCODING) - # 0 is the default language (localization related) - text = self._PR_ErrorToString(code, 0) - text = text.decode(SYS_ENCODING) - - LOG.debug("%s: %s", name, text) - - def decode(self, data64): - data = b64decode(data64) - inp = self.SECItem(0, data, len(data)) - out = self.SECItem(0, None, 0) - - e = self._PK11SDR_Decrypt(inp, out, None) - LOG.debug("Decryption of data returned %s", e) - try: - if e == -1: - LOG.error("Password decryption failed. Passwords protected by a Master Password!") - self.handle_error() - raise Exit(Exit.NEED_MASTER_PASSWORD) - - res = ct.string_at(out.data, out.len).decode(LIB_ENCODING) - finally: - # Avoid leaking SECItem - self._SECITEM_ZfreeItem(out, 0) - - return res - - -class NSSInteraction(object): - """ - Interact with lib NSS - """ - def __init__(self): - self.profile = None - self.NSS = NSSDecoder() - - def load_profile(self, profile): - """Initialize the NSS library and profile - """ - LOG.debug("Initializing NSS with profile path '%s'", profile) - self.profile = profile - - profile = profile.encode(LIB_ENCODING) - - e = self.NSS._NSS_Init(b"sql:" + profile) - LOG.debug("Initializing NSS returned %s", e) - - if e != 0: - LOG.error("Couldn't initialize NSS, maybe '%s' is not a valid profile?", self.profile) - self.NSS.handle_error() - raise Exit(Exit.FAIL_INIT_NSS) - - def authenticate(self, interactive): - """Check if the current profile is protected by a master password, - prompt the user and unlock the profile. - """ - LOG.debug("Retrieving internal key slot") - keyslot = self.NSS._PK11_GetInternalKeySlot() - - LOG.debug("Internal key slot %s", keyslot) - if not keyslot: - LOG.error("Failed to retrieve internal KeySlot") - self.NSS.handle_error() - raise Exit(Exit.FAIL_NSS_KEYSLOT) - - try: - # NOTE It would be great to be able to check if the profile is - # protected by a master password. In C++ one would do: - # if (keyslot->needLogin): - # however accessing instance methods is not supported by ctypes. - # More on this topic: http://stackoverflow.com/a/19636310 - # A possibility would be to define such function using cython but - # this adds an unnecessary runtime dependency - password = ask_password(self.profile, interactive) - - if password: - LOG.debug("Authenticating with password '%s'", password) - e = self.NSS._PK11_CheckUserPassword(keyslot, password.encode(LIB_ENCODING)) - - LOG.debug("Checking user password returned %s", e) - - if e != 0: - LOG.error("Master password is not correct") - - self.NSS.handle_error() - raise Exit(Exit.BAD_MASTER_PASSWORD) - - else: - LOG.warning("Attempting decryption with no Master Password") - finally: - # Avoid leaking PK11KeySlot - self.NSS._PK11_FreeSlot(keyslot) - - def unload_profile(self): - """Shutdown NSS and deactivate current profile - """ - e = self.NSS._NSS_Shutdown() - - if e != 0: - LOG.error("Couldn't shutdown current NSS profile") - - self.NSS.handle_error() - raise Exit(Exit.FAIL_SHUTDOWN_NSS) - - def decode_entry(self, user64, passw64): - """Decrypt one entry in the database - """ - LOG.debug("Decrypting username data '%s'", user64) - user = self.NSS.decode(user64) - - LOG.debug("Decrypting password data '%s'", passw64) - passw = self.NSS.decode(passw64) - - return user, passw - - def decrypt_passwords(self, export, output_format="human", csv_delimiter=";", csv_quotechar="|"): - """ - Decrypt requested profile using the provided password and print out all - stored passwords. - """ - # Any password in this profile store at all? - got_password = False - header = True - - credentials = obtain_credentials(self.profile) - - LOG.info("Decrypting credentials") - to_export = {} - outputs = [] - - if output_format == "csv": - csv_writer = csv.DictWriter( - sys.stdout, fieldnames=["url", "user", "password"], - lineterminator="\n", delimiter=csv_delimiter, - quotechar=csv_quotechar, quoting=csv.QUOTE_ALL, - ) - if header: - csv_writer.writeheader() - - for url, user, passw, enctype in credentials: - got_password = True - - # enctype informs if passwords are encrypted and protected by - # a master password - if enctype: - user, passw = self.decode_entry(user, passw) - - LOG.debug("Decoding username '%s' and password '%s' for website '%s'", user, passw, url) - LOG.debug("Decoding username '%s' and password '%s' for website '%s'", type(user), type(passw), type(url)) - - if export: - # Keep track of web-address, username and passwords - # If more than one username exists for the same web-address - # the username will be used as name of the file - address = urlparse(url) - - if address.netloc not in to_export: - to_export[address.netloc] = {user: passw} - - else: - to_export[address.netloc][user] = passw - - if output_format == "csv": - output = {"url": url, "user": user, "password": passw} - if PY3: - csv_writer.writerow(output) - else: - csv_writer.writerow({k: v.encode(USR_ENCODING) for k, v in output.items()}) - elif output_format == "json": - output = {"url": url, "user": user, "password": passw} - outputs.append(output) - - else: - output = ( - u"\nWebsite: {0}\n".format(url), - u"Username: '{0}'\n".format(user), - u"Password: '{0}'\n".format(passw), - ) - for line in output: - sys.stdout.write(py2_encode(line, USR_ENCODING)) - if output_format == "json": - print(json.dumps(outputs)) - - - credentials.done() - - if not got_password: - LOG.warning("No passwords found in selected profile") - - if export: - return to_export - - -def test_password_store(export, pass_cmd): - """Check if pass from passwordstore.org is installed - If it is installed but not initialized, initialize it - """ - # Nothing to do here if exporting wasn't requested - if not export: - LOG.debug("Skipping password store test, not exporting") - return - - LOG.debug("Testing if password store is installed and configured") - - try: - p = Popen([pass_cmd], stdout=PIPE, stderr=PIPE) - except OSError as e: - if e.errno == 2: - LOG.error("Password store is not installed and exporting was requested") - raise Exit(Exit.PASSSTORE_MISSING) - else: - LOG.error("Unknown error happened.") - LOG.error("Error was %s", e) - raise Exit(Exit.UNKNOWN_ERROR) - - out, err = p.communicate() - LOG.debug("pass returned: %s %s", out, err) - - if p.returncode != 0: - if 'Try "pass init"' in err: - LOG.error("Password store was not initialized.") - LOG.error("Initialize the password store manually by using 'pass init'") - raise Exit(Exit.PASSSTORE_NOT_INIT) - else: - LOG.error("Unknown error happened when running 'pass'.") - LOG.error("Stdout/Stderr was '%s' '%s'", out, err) - raise Exit(Exit.UNKNOWN_ERROR) - - -def obtain_credentials(profile): - """Figure out which of the 2 possible backend credential engines is available - """ - try: - credentials = JsonCredentials(profile) - except NotFoundError: - try: - credentials = SqliteCredentials(profile) - except NotFoundError: - LOG.error("Couldn't find credentials file (logins.json or signons.sqlite).") - raise Exit(Exit.MISSING_SECRETS) - - return credentials - - -def export_pass(to_export, pass_cmd, prefix, username_prefix): - """Export given passwords to password store - - Format of "to_export" should be: - {"address": {"login": "password", ...}, ...} - """ - LOG.info("Exporting credentials to password store") - if prefix: - prefix = u"{0}/".format(prefix) - - LOG.debug("Using pass prefix '%s'", prefix) - - for address in to_export: - for user, passw in to_export[address].items(): - # When more than one account exist for the same address, add - # the login to the password identifier - if len(to_export[address]) > 1: - passname = u"{0}{1}/{2}".format(prefix, address, user) - - else: - passname = u"{0}{1}".format(prefix, address) - - LOG.debug("Exporting credentials for '%s'", passname) - - data = u"{0}\n{1}{2}\n".format(passw, username_prefix, user) - - LOG.debug("Inserting pass '%s' '%s'", passname, data) - - # NOTE --force is used. Existing passwords will be overwritten - cmd = [pass_cmd, "insert", "--force", "--multiline", passname] - - LOG.debug("Running command '%s' with stdin '%s'", cmd, data) - - p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) - out, err = p.communicate(data.encode(SYS_ENCODING)) - - if p.returncode != 0: - LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode) - LOG.error("Stdout/Stderr was '%s' '%s'", out, err) - raise Exit(Exit.PASSSTORE_ERROR) - - LOG.debug("Successfully exported '%s'", passname) - - -def get_sections(profiles): - """ - Returns hash of profile numbers and profile names. - """ - sections = {} - i = 1 - for section in profiles.sections(): - if section.startswith("Profile"): - sections[str(i)] = profiles.get(section, "Path") - i += 1 - else: - continue - return sections - - -def print_sections(sections, textIOWrapper=sys.stderr): - """ - Prints all available sections to an textIOWrapper (defaults to sys.stderr) - """ - for i in sorted(sections): - textIOWrapper.write("{0} -> {1}\n".format(i, sections[i])) - textIOWrapper.flush() - - -def ask_section(profiles, choice_arg): - """ - Prompt the user which profile should be used for decryption - """ - sections = get_sections(profiles) - - # Do not ask for choice if user already gave one - if choice_arg and len(choice_arg) == 1: - choice = choice_arg[0] - else: - # If only one menu entry exists, use it without prompting - if len(sections) == 1: - choice = "1" - - else: - choice = None - while choice not in sections: - sys.stderr.write("Select the Firefox profile you wish to decrypt\n") - print_sections(sections) - try: - choice = raw_input() - except EOFError: - LOG.error("Could not read Choice, got EOF") - raise Exit(Exit.READ_GOT_EOF) - - try: - final_choice = sections[choice] - except KeyError: - LOG.error("Profile No. %s does not exist!", choice) - raise Exit(Exit.NO_SUCH_PROFILE) - - LOG.debug("Profile selection matched %s", final_choice) - - return final_choice - - -def ask_password(profile, interactive): - """ - Prompt for profile password - """ - if not PY3: - profile = profile.encode(SYS_ENCODING) - - passmsg = "\nMaster Password for profile {0}: ".format(profile) - - if sys.stdin.isatty() and interactive: - passwd = getpass(passmsg) - - else: - # Ability to read the password from stdin (echo "pass" | ./firefox_...) - if sys.stdin in select.select([sys.stdin], [], [], 0)[0]: - passwd = sys.stdin.readline().rstrip("\n") - else: - LOG.warning("Master Password not provided, continuing with blank password") - passwd = "" - - return py2_decode(passwd) - - -def read_profiles(basepath, list_profiles): - """ - Parse Firefox profiles in provided location. - If list_profiles is true, will exit after listing available profiles. - """ - profileini = os.path.join(basepath, "profiles.ini") - - LOG.debug("Reading profiles from %s", profileini) - - if not os.path.isfile(profileini): - LOG.warning("profile.ini not found in %s", basepath) - raise Exit(Exit.MISSING_PROFILEINI) - - # Read profiles from Firefox profile folder - profiles = ConfigParser() - profiles.read(profileini) - - LOG.debug("Read profiles %s", profiles.sections()) - - if list_profiles: - LOG.debug("Listing available profiles...") - print_sections(get_sections(profiles), sys.stdout) - raise Exit(0) - - return profiles - - -def get_profile(basepath, interactive, choice, list_profiles): - """ - Select profile to use by either reading profiles.ini or assuming given - path is already a profile - If interactive is false, will not try to ask which profile to decrypt. - choice contains the choice the user gave us as an CLI arg. - If list_profiles is true will exits after listing all available profiles. - """ - try: - profiles = read_profiles(basepath, list_profiles) - except Exit as e: - if e.exitcode == Exit.MISSING_PROFILEINI: - LOG.warning("Continuing and assuming '%s' is a profile location", basepath) - profile = basepath - - if list_profiles: - LOG.error("Listing single profiles not permitted.") - raise - - if not os.path.isdir(profile): - LOG.error("Profile location '%s' is not a directory", profile) - raise - else: - raise - else: - if not interactive: - sections = get_sections(profiles) - - if choice and len(choice) == 1: - try: - section = sections[(choice[0])] - except KeyError: - LOG.error("Profile No. %s does not exist!", choice[0]) - raise Exit(Exit.NO_SUCH_PROFILE) - - elif len(sections) == 1: - section = sections['1'] - - else: - LOG.error("Don't know which profile to decrypt. We are in non-interactive mode and -c/--choice is missing.") - raise Exit(Exit.MISSING_CHOICE) - else: - # Ask user which profile to open - section = ask_section(profiles, choice) - - section = py2_decode(section, LIB_ENCODING) - profile = os.path.join(basepath, section) - - if not os.path.isdir(profile): - LOG.error("Profile location '%s' is not a directory. Has profiles.ini been tampered with?", profile) - raise Exit(Exit.BAD_PROFILEINI) - - return profile - - -def parse_sys_args(): - """Parse command line arguments - """ - - if os.name == "nt": - profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox") - elif os.uname()[0] == "Darwin": - profile_path = "~/Library/Application Support/Firefox" - else: - profile_path = "~/.mozilla/firefox" - - parser = argparse.ArgumentParser( - description="Access Firefox/Thunderbird profiles and decrypt existing passwords" - ) - parser.add_argument("profile", nargs="?", default=profile_path, - type=type_decode(SYS_ENCODING), - help="Path to profile folder (default: {0})".format(profile_path)) - parser.add_argument("-e", "--export-pass", action="store_true", - help="Export URL, username and password to pass from passwordstore.org") - parser.add_argument("--pass-compat", action="store", - choices={"default", "browserpass", "username"}, - default="default", - help="Export username as is (default), or with one of the compatibility modes") - parser.add_argument("-p", "--pass-prefix", action="store", default=u"web", - help="Prefix for export to pass from passwordstore.org (default: %(default)s)") - parser.add_argument("-m", "--pass-cmd", action="store", default=u"pass", - help="Command/path to use when exporting to pass (default: %(default)s)") - parser.add_argument("-f", "--format", action="store", choices={"csv", "human", "json"}, - default="human", help="Format for the output.") - parser.add_argument("-d", "--delimiter", action="store", default=";", - help="The delimiter for csv output") - parser.add_argument("-q", "--quotechar", action="store", default='"', - help="The quote char for csv output") - parser.add_argument("-t", "--tabular", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("-n", "--no-interactive", dest="interactive", - default=True, action="store_false", - help="Disable interactivity.") - parser.add_argument("-c", "--choice", nargs=1, - help="The profile to use (starts with 1). If only one profile, defaults to that.") - parser.add_argument("-l", "--list", action="store_true", - help="List profiles and exit.") - parser.add_argument("-v", "--verbose", action="count", default=0, - help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen") - parser.add_argument("--version", action="version", version=__version__, - help="Display version of firefox_decrypt and exit") - - args = parser.parse_args() - - # replace character you can't enter as argument - if args.delimiter == "\\t": - args.delimiter = "\t" - - if args.tabular: - args.format = "csv" - args.delimiter = "\t" - args.quotechar = "'" - - return args - - -def setup_logging(args): - """Setup the logging level and configure the basic logger - """ - if args.verbose == 1: - level = logging.INFO - elif args.verbose >= 2: - level = logging.DEBUG - else: - level = logging.WARN - - logging.basicConfig( - format="%(asctime)s - %(levelname)s - %(message)s", - level=level, - ) - - global LOG - LOG = logging.getLogger(__name__) - - -def main(): - """Main entry point - """ - args = parse_sys_args() - - setup_logging(args) - - if args.tabular: - LOG.warning("--tabular is deprecated. Use `--format csv --delimiter \\t` instead") - - LOG.info("Running firefox_decrypt version: %s", __version__) - LOG.debug("Parsed commandline arguments: %s", args) - LOG.debug("Running with encodings: USR: %s, SYS: %s, LIB: %s", USR_ENCODING, SYS_ENCODING, LIB_ENCODING) - - # Check whether pass from passwordstore.org is installed - test_password_store(args.export_pass, args.pass_cmd) - - # Initialize nss before asking the user for input - nss = NSSInteraction() - - basepath = os.path.expanduser(args.profile) - - # Read profiles from profiles.ini in profile folder - profile = get_profile(basepath, args.interactive, args.choice, args.list) - - # Start NSS for selected profile - nss.load_profile(profile) - # Check if profile is password protected and prompt for a password - nss.authenticate(args.interactive) - # Decode all passwords - to_export = nss.decrypt_passwords( - export=args.export_pass, - output_format=args.format, - csv_delimiter=args.delimiter, - csv_quotechar=args.quotechar, - ) - - if args.export_pass: - # List of compatibility modes for username prefixes - compat = { - "username": "username: ", - "browserpass": "login: ", - } - - username_prefix = compat.get(args.pass_compat, "") - export_pass(to_export, args.pass_cmd, args.pass_prefix, username_prefix) - - # And shutdown NSS - nss.unload_profile() - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt as e: - print("Quit.") - sys.exit(Exit.KEYBOARD_INTERRUPT) - except Exit as e: - sys.exit(e.exitcode)