]> git.nbdom.net Git - nb.git/commitdiff
/opt/nb/bin/firefox_decrypt.py
authorNicolas Boisselier <nicolas.boisselier@gmail.com>
Fri, 6 Sep 2019 23:33:19 +0000 (00:33 +0100)
committerNicolas Boisselier <nicolas.boisselier@gmail.com>
Fri, 6 Sep 2019 23:33:19 +0000 (00:33 +0100)
bin/firefox_decrypt.py [deleted file]

diff --git a/bin/firefox_decrypt.py b/bin/firefox_decrypt.py
deleted file mode 100755 (executable)
index c86afe8..0000000
+++ /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 <http://www.gnu.org/licenses/>.
-
-# 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)