mirror of
https://github.com/unode/firefox_decrypt.git
synced 2025-12-16 12:01:52 +01:00
@@ -5,6 +5,8 @@
|
||||
- Allow overriding default encoding
|
||||
- Add `--pass-always-with-login` to always include /login as part of pass's password path
|
||||
- Improve compatibility with `gopass` by explicitly using `pass ls` instead of `pass`
|
||||
- Add `--non-fatal-decryption` to attempt decrypting partially corrupt databases instead of aborting on first failure
|
||||
- Enable All Contributors framework in project
|
||||
|
||||
##### 1.0.0
|
||||
- Improve detection of NSS in Windows and MacOS
|
||||
|
||||
19
README.md
19
README.md
@@ -83,7 +83,7 @@ You can also choose from one of the supported formats with `--format`:
|
||||
* `json` - a machine compatible format - see [JSON](https://en.wikipedia.org/wiki/JSON)
|
||||
* `pass` - a special output format that directly calls to the [passwordstore.org](https://www.passwordstore.org) command to export passwords (*). See also `--pass-*` options.
|
||||
|
||||
(*) `pass` can produce unintended consequences. Make sure to backup your password store before using this.
|
||||
(*) `pass` can produce unintended consequences. Make sure to backup your password store before using this option.
|
||||
|
||||
##### Non-interactive mode
|
||||
|
||||
@@ -164,6 +164,19 @@ There is currently no way to selectively export passwords.
|
||||
|
||||
Exporting will overwrite existing passwords without warning. Ensure you have a backup or are using the `pass git` functionality.
|
||||
|
||||
##### Non fatal password decryption
|
||||
|
||||
By default, encountering a corrupted username or password will abort decryption.
|
||||
Since version `1.1.0` there is now `--non-fatal-decryption` that tolerates individual failures.
|
||||
|
||||
$ python firefox_decrypt.py --non-fatal-decryption
|
||||
(...)
|
||||
Website: https://github.com
|
||||
Username: '*** decryption failed ***'
|
||||
Password: '*** decryption failed ***'
|
||||
|
||||
which can also be combined with any of the above `--format` options.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
If a problem occurs, please try `firefox_decrypt` in high verbosity mode by calling it with:
|
||||
@@ -176,6 +189,10 @@ If the output does not help you to identify the cause and a solution to the prob
|
||||
|
||||
- your profile password, as well as other passwords, may be visible in the output – so **please remove any sensitive data** before sharing the output.
|
||||
|
||||
##### Silencing error messages
|
||||
|
||||
Logging messages above warning level are included in the standard error output by default as these can be useful to troubleshoot failures.
|
||||
If you wish to omit this information append ` 2>/dev/null` to your command on UNIX and ` 2> nul` on Windows.
|
||||
|
||||
##### Windows
|
||||
|
||||
|
||||
@@ -59,8 +59,9 @@ def get_version() -> str:
|
||||
"""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:])
|
||||
return ".".join(map(str, __version_info__[:3])) + "".join(__version_info__[3:])
|
||||
|
||||
try:
|
||||
p = run(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL, text=True)
|
||||
@@ -78,14 +79,14 @@ __version__: str = get_version()
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
"""Exception to handle situations where a credentials file is not found
|
||||
"""
|
||||
"""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
|
||||
"""
|
||||
"""Exception to allow a clean exit from any point in execution"""
|
||||
|
||||
CLEAN = 0
|
||||
ERROR = 1
|
||||
MISSING_PROFILEINI = 2
|
||||
@@ -123,8 +124,8 @@ class Exit(Exception):
|
||||
|
||||
|
||||
class Credentials:
|
||||
"""Base credentials backend manager
|
||||
"""
|
||||
"""Base credentials backend manager"""
|
||||
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
@@ -145,8 +146,8 @@ class Credentials:
|
||||
|
||||
|
||||
class SqliteCredentials(Credentials):
|
||||
"""SQLite credentials backend manager
|
||||
"""
|
||||
"""SQLite credentials backend manager"""
|
||||
|
||||
def __init__(self, profile):
|
||||
db = os.path.join(profile, "signons.sqlite")
|
||||
|
||||
@@ -157,15 +158,16 @@ class SqliteCredentials(Credentials):
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[str, str, str, int]]:
|
||||
LOG.debug("Reading password database in SQLite format")
|
||||
self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType "
|
||||
"FROM moz_logins")
|
||||
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
|
||||
"""
|
||||
"""Close the sqlite cursor and database connection"""
|
||||
super(SqliteCredentials, self).done()
|
||||
|
||||
self.c.close()
|
||||
@@ -173,8 +175,8 @@ class SqliteCredentials(Credentials):
|
||||
|
||||
|
||||
class JsonCredentials(Credentials):
|
||||
"""JSON credentials backend manager
|
||||
"""
|
||||
"""JSON credentials backend manager"""
|
||||
|
||||
def __init__(self, profile):
|
||||
db = os.path.join(profile, "logins.json")
|
||||
|
||||
@@ -192,13 +194,16 @@ class JsonCredentials(Credentials):
|
||||
raise Exit(Exit.BAD_SECRETS)
|
||||
|
||||
for i in logins:
|
||||
yield (i["hostname"], i["encryptedUsername"],
|
||||
i["encryptedPassword"], i["encType"])
|
||||
yield (
|
||||
i["hostname"],
|
||||
i["encryptedUsername"],
|
||||
i["encryptedPassword"],
|
||||
i["encType"],
|
||||
)
|
||||
|
||||
|
||||
def find_nss(locations, nssname) -> ct.CDLL:
|
||||
"""Locate nss is one of the many possible locations
|
||||
"""
|
||||
"""Locate nss is one of the many possible locations"""
|
||||
fail_errors: list[tuple[str, str]] = []
|
||||
|
||||
OS = ("Windows", "Darwin")
|
||||
@@ -210,7 +215,7 @@ def find_nss(locations, nssname) -> ct.CDLL:
|
||||
if SYSTEM in OS:
|
||||
# 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"]])
|
||||
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
|
||||
@@ -236,18 +241,29 @@ def find_nss(locations, nssname) -> ct.CDLL:
|
||||
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(
|
||||
"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:")
|
||||
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, error)
|
||||
@@ -256,8 +272,7 @@ def find_nss(locations, nssname) -> ct.CDLL:
|
||||
|
||||
|
||||
def load_libnss():
|
||||
"""Load libnss into python using the CDLL interface
|
||||
"""
|
||||
"""Load libnss into python using the CDLL interface"""
|
||||
if SYSTEM == "Windows":
|
||||
nssname = "nss3.dll"
|
||||
locations: list[str] = [
|
||||
@@ -348,18 +363,19 @@ def load_libnss():
|
||||
|
||||
class c_char_p_fromstr(ct.c_char_p):
|
||||
"""ctypes char_p override that handles encoding str to bytes"""
|
||||
|
||||
def from_param(self):
|
||||
return self.encode(DEFAULT_ENCODING)
|
||||
|
||||
|
||||
class NSSProxy:
|
||||
class SECItem(ct.Structure):
|
||||
"""struct needed to interact with libnss
|
||||
"""
|
||||
"""struct needed to interact with libnss"""
|
||||
|
||||
_fields_ = [
|
||||
('type', ct.c_uint),
|
||||
('data', ct.c_char_p), # actually: unsigned char *
|
||||
('len', ct.c_uint),
|
||||
("type", ct.c_uint),
|
||||
("data", ct.c_char_p), # actually: unsigned char *
|
||||
("len", ct.c_uint),
|
||||
]
|
||||
|
||||
def decode_data(self):
|
||||
@@ -367,12 +383,12 @@ class NSSProxy:
|
||||
return _bytes.decode(DEFAULT_ENCODING)
|
||||
|
||||
class PK11SlotInfo(ct.Structure):
|
||||
"""Opaque structure representing a logical PKCS slot
|
||||
"""
|
||||
"""Opaque structure representing a logical PKCS slot"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, non_fatal_decryption=False):
|
||||
# Locate libnss and try loading it
|
||||
self.libnss = load_libnss()
|
||||
self.non_fatal_decryption = non_fatal_decryption
|
||||
|
||||
SlotInfoPtr = ct.POINTER(self.PK11SlotInfo)
|
||||
SECItemPtr = ct.POINTER(self.SECItem)
|
||||
@@ -382,8 +398,12 @@ class NSSProxy:
|
||||
self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot")
|
||||
self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr)
|
||||
self._set_ctypes(ct.c_int, "PK11_NeedLogin", SlotInfoPtr)
|
||||
self._set_ctypes(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr)
|
||||
self._set_ctypes(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p)
|
||||
self._set_ctypes(
|
||||
ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, c_char_p_fromstr
|
||||
)
|
||||
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
|
||||
@@ -392,19 +412,20 @@ class NSSProxy:
|
||||
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
|
||||
"""
|
||||
"""Set input/output types on libnss C functions for automatic type casting"""
|
||||
res = getattr(self.libnss, name)
|
||||
res.argtypes = argtypes
|
||||
res.restype = restype
|
||||
|
||||
# Transparently handle decoding to string when returning a c_char_p
|
||||
if restype == ct.c_char_p:
|
||||
|
||||
def _decode(result, func, *args):
|
||||
try:
|
||||
return result.decode(DEFAULT_ENCODING)
|
||||
except AttributeError:
|
||||
return result
|
||||
|
||||
res.errcheck = _decode
|
||||
|
||||
setattr(self, "_" + name, res)
|
||||
@@ -469,8 +490,7 @@ class NSSProxy:
|
||||
self._PK11_FreeSlot(keyslot)
|
||||
|
||||
def handle_error(self, exitcode: int, *logerror: Any):
|
||||
"""If an error happens in libnss, handle it and print some debug information
|
||||
"""
|
||||
"""If an error happens in libnss, handle it and print some debug information"""
|
||||
if logerror:
|
||||
LOG.error(*logerror)
|
||||
else:
|
||||
@@ -495,12 +515,16 @@ class NSSProxy:
|
||||
LOG.debug("Decryption of data returned %s", err_status)
|
||||
try:
|
||||
if err_status: # -1 means password failed, other status are unknown
|
||||
self.handle_error(
|
||||
Exit.DECRYPTION_FAILED,
|
||||
error_msg = (
|
||||
"Username/Password decryption failed. "
|
||||
"Credentials damaged or cert/key file mismatch.",
|
||||
"Credentials damaged or cert/key file mismatch."
|
||||
)
|
||||
|
||||
if self.non_fatal_decryption:
|
||||
raise ValueError(error_msg)
|
||||
else:
|
||||
self.handle_error(Exit.DECRYPTION_FAILED, error_msg)
|
||||
|
||||
res = out.decode_data()
|
||||
finally:
|
||||
# Avoid leaking SECItem
|
||||
@@ -513,13 +537,13 @@ class MozillaInteraction:
|
||||
"""
|
||||
Abstraction interface to Mozilla profile and lib NSS
|
||||
"""
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, non_fatal_decryption=False):
|
||||
self.profile = None
|
||||
self.proxy = NSSProxy()
|
||||
self.proxy = NSSProxy(non_fatal_decryption)
|
||||
|
||||
def load_profile(self, profile):
|
||||
"""Initialize the NSS library and profile
|
||||
"""
|
||||
"""Initialize the NSS library and profile"""
|
||||
self.profile = profile
|
||||
self.proxy.initialize(self.profile)
|
||||
|
||||
@@ -530,8 +554,7 @@ class MozillaInteraction:
|
||||
self.proxy.authenticate(self.profile, interactive)
|
||||
|
||||
def unload_profile(self):
|
||||
"""Shutdown NSS and deactivate current profile
|
||||
"""
|
||||
"""Shutdown NSS and deactivate current profile"""
|
||||
self.proxy.shutdown()
|
||||
|
||||
def decrypt_passwords(self) -> PWStore:
|
||||
@@ -541,7 +564,7 @@ class MozillaInteraction:
|
||||
credentials: Credentials = self.obtain_credentials()
|
||||
|
||||
LOG.info("Decrypting credentials")
|
||||
outputs: list[dict[str, str]] = []
|
||||
outputs: PWStore = []
|
||||
|
||||
url: str
|
||||
user: str
|
||||
@@ -556,11 +579,20 @@ class MozillaInteraction:
|
||||
LOG.debug("Decrypting password data '%s'", passw)
|
||||
passw = self.proxy.decrypt(passw)
|
||||
except (TypeError, ValueError) as e:
|
||||
LOG.warning("Failed to decode username or password for entry from URL %s", url)
|
||||
LOG.warning(
|
||||
"Failed to decode username or password for entry from URL %s",
|
||||
url,
|
||||
)
|
||||
LOG.exception(e)
|
||||
continue
|
||||
user = "*** decryption failed ***"
|
||||
passw = "*** decryption failed ***"
|
||||
|
||||
LOG.debug("Decoded username '%s' and password '%s' for website '%s'", user, passw, url)
|
||||
LOG.debug(
|
||||
"Decoded username '%s' and password '%s' for website '%s'",
|
||||
user,
|
||||
passw,
|
||||
url,
|
||||
)
|
||||
|
||||
output = {"url": url, "user": user, "password": passw}
|
||||
outputs.append(output)
|
||||
@@ -574,8 +606,7 @@ class MozillaInteraction:
|
||||
return outputs
|
||||
|
||||
def obtain_credentials(self) -> Credentials:
|
||||
"""Figure out which of the 2 possible backend credential engines is available
|
||||
"""
|
||||
"""Figure out which of the 2 possible backend credential engines is available"""
|
||||
credentials: Credentials
|
||||
try:
|
||||
credentials = JsonCredentials(self.profile)
|
||||
@@ -583,7 +614,9 @@ class MozillaInteraction:
|
||||
try:
|
||||
credentials = SqliteCredentials(self.profile)
|
||||
except NotFoundError:
|
||||
LOG.error("Couldn't find credentials file (logins.json or signons.sqlite).")
|
||||
LOG.error(
|
||||
"Couldn't find credentials file (logins.json or signons.sqlite)."
|
||||
)
|
||||
raise Exit(Exit.MISSING_SECRETS)
|
||||
|
||||
return credentials
|
||||
@@ -739,14 +772,22 @@ class PassOutputFormat(OutputFormat):
|
||||
LOG.debug("Inserting pass '%s' '%s'", passname, data)
|
||||
|
||||
# NOTE --force is used. Existing passwords will be overwritten
|
||||
cmd: list[str] = [self.cmd, "insert", "--force", "--multiline", passname]
|
||||
cmd: list[str] = [
|
||||
self.cmd,
|
||||
"insert",
|
||||
"--force",
|
||||
"--multiline",
|
||||
passname,
|
||||
]
|
||||
|
||||
LOG.debug("Running command '%s' with stdin '%s'", cmd, data)
|
||||
|
||||
p = run(cmd, input=data, capture_output=True, text=True)
|
||||
|
||||
if p.returncode != 0:
|
||||
LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode)
|
||||
LOG.error(
|
||||
"ERROR: passwordstore exited with non-zero: %s", p.returncode
|
||||
)
|
||||
LOG.error("Stdout: %s\nStderr: %s", p.stdout, p.stderr)
|
||||
raise Exit(Exit.PASSSTORE_ERROR)
|
||||
|
||||
@@ -843,7 +884,9 @@ def read_profiles(basepath):
|
||||
return profiles
|
||||
|
||||
|
||||
def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool):
|
||||
def get_profile(
|
||||
basepath: str, interactive: bool, choice: Optional[str], list_profiles: bool
|
||||
):
|
||||
"""
|
||||
Select profile to use by either reading profiles.ini or assuming given
|
||||
path is already a profile
|
||||
@@ -887,8 +930,10 @@ def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_pr
|
||||
raise Exit(Exit.NO_SUCH_PROFILE)
|
||||
|
||||
elif not interactive:
|
||||
LOG.error("Don't know which profile to decrypt. "
|
||||
"We are in non-interactive mode and -c/--choice wasn't specified.")
|
||||
LOG.error(
|
||||
"Don't know which profile to decrypt. "
|
||||
"We are in non-interactive mode and -c/--choice wasn't specified."
|
||||
)
|
||||
raise Exit(Exit.MISSING_CHOICE)
|
||||
|
||||
else:
|
||||
@@ -899,7 +944,10 @@ def get_profile(basepath: str, interactive: bool, choice: Optional[str], list_pr
|
||||
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)
|
||||
LOG.error(
|
||||
"Profile location '%s' is not a directory. Has profiles.ini been tampered with?",
|
||||
profile,
|
||||
)
|
||||
raise Exit(Exit.BAD_PROFILEINI)
|
||||
|
||||
return profile
|
||||
@@ -911,6 +959,7 @@ class ConvertChoices(argparse.Action):
|
||||
mapping the user-specified choices values to the resulting option
|
||||
values.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, choices, **kwargs):
|
||||
super().__init__(*args, choices=choices.keys(), **kwargs)
|
||||
self.mapping = choices
|
||||
@@ -920,11 +969,10 @@ class ConvertChoices(argparse.Action):
|
||||
|
||||
|
||||
def parse_sys_args() -> argparse.Namespace:
|
||||
"""Parse command line arguments
|
||||
"""
|
||||
"""Parse command line arguments"""
|
||||
|
||||
if SYSTEM == "Windows":
|
||||
profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox")
|
||||
profile_path = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox")
|
||||
elif os.uname()[0] == "Darwin":
|
||||
profile_path = "~/Library/Application Support/Firefox"
|
||||
else:
|
||||
@@ -935,8 +983,10 @@ def parse_sys_args() -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument(
|
||||
"profile",
|
||||
nargs="?", default=profile_path,
|
||||
help=f"Path to profile folder (default: {profile_path})")
|
||||
nargs="?",
|
||||
default=profile_path,
|
||||
help=f"Path to profile folder (default: {profile_path})",
|
||||
)
|
||||
|
||||
format_choices = {
|
||||
"human": HumanOutputFormat,
|
||||
@@ -947,25 +997,34 @@ def parse_sys_args() -> argparse.Namespace:
|
||||
}
|
||||
|
||||
parser.add_argument(
|
||||
"-f", "--format",
|
||||
"-f",
|
||||
"--format",
|
||||
action=ConvertChoices,
|
||||
choices=format_choices, default=HumanOutputFormat,
|
||||
help="Format for the output.")
|
||||
choices=format_choices,
|
||||
default=HumanOutputFormat,
|
||||
help="Format for the output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--csv-delimiter",
|
||||
"-d",
|
||||
"--csv-delimiter",
|
||||
action="store",
|
||||
default=";",
|
||||
help="The delimiter for csv output")
|
||||
help="The delimiter for csv output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q", "--csv-quotechar",
|
||||
"-q",
|
||||
"--csv-quotechar",
|
||||
action="store",
|
||||
default='"',
|
||||
help="The quote char for csv output")
|
||||
help="The quote char for csv output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-csv-header",
|
||||
action="store_false", dest="csv_header",
|
||||
action="store_false",
|
||||
dest="csv_header",
|
||||
default=True,
|
||||
help="Do not include a header in CSV output.")
|
||||
help="Do not include a header in CSV output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pass-username-prefix",
|
||||
action="store",
|
||||
@@ -973,47 +1032,69 @@ def parse_sys_args() -> argparse.Namespace:
|
||||
help=(
|
||||
"Export username as is (default), or with the provided format prefix. "
|
||||
"For instance 'login: ' for browserpass."
|
||||
))
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--pass-prefix",
|
||||
"-p",
|
||||
"--pass-prefix",
|
||||
action="store",
|
||||
default="web",
|
||||
help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)")
|
||||
help="Folder prefix for export to pass from passwordstore.org (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--pass-cmd",
|
||||
"-m",
|
||||
"--pass-cmd",
|
||||
action="store",
|
||||
default="pass",
|
||||
help="Command/path to use when exporting to pass (default: %(default)s)")
|
||||
help="Command/path to use when exporting to pass (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pass-always-with-login",
|
||||
action="store_true",
|
||||
help="Always save as /<login> (default: only when multiple accounts per domain)")
|
||||
help="Always save as /<login> (default: only when multiple accounts per domain)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--no-interactive",
|
||||
action="store_false", dest="interactive",
|
||||
"-n",
|
||||
"--no-interactive",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
default=True,
|
||||
help="Disable interactivity.")
|
||||
help="Disable interactivity.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--choice",
|
||||
help="The profile to use (starts with 1). If only one profile, defaults to that.")
|
||||
parser.add_argument(
|
||||
"-l", "--list",
|
||||
"--non-fatal-decryption",
|
||||
action="store_true",
|
||||
help="List profiles and exit.")
|
||||
default=False,
|
||||
help="If set, corrupted entries will be skipped instead of aborting the process.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--encoding",
|
||||
"-c",
|
||||
"--choice",
|
||||
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(
|
||||
"-e",
|
||||
"--encoding",
|
||||
action="store",
|
||||
default=DEFAULT_ENCODING,
|
||||
help="Override default encoding (%(default)s).")
|
||||
help="Override default encoding (%(default)s).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen")
|
||||
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")
|
||||
action="version",
|
||||
version=__version__,
|
||||
help="Display version of firefox_decrypt and exit",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -1025,8 +1106,7 @@ def parse_sys_args() -> argparse.Namespace:
|
||||
|
||||
|
||||
def setup_logging(args) -> None:
|
||||
"""Setup the logging level and configure the basic logger
|
||||
"""
|
||||
"""Setup the logging level and configure the basic logger"""
|
||||
if args.verbose == 1:
|
||||
level = logging.INFO
|
||||
elif args.verbose >= 2:
|
||||
@@ -1059,8 +1139,7 @@ def identify_system_locale() -> str:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point
|
||||
"""
|
||||
"""Main entry point"""
|
||||
args = parse_sys_args()
|
||||
|
||||
setup_logging(args)
|
||||
@@ -1068,8 +1147,11 @@ def main() -> None:
|
||||
global DEFAULT_ENCODING
|
||||
|
||||
if args.encoding != DEFAULT_ENCODING:
|
||||
LOG.info("Overriding default encoding from '%s' to '%s'",
|
||||
DEFAULT_ENCODING, args.encoding)
|
||||
LOG.info(
|
||||
"Overriding default encoding from '%s' to '%s'",
|
||||
DEFAULT_ENCODING,
|
||||
args.encoding,
|
||||
)
|
||||
|
||||
# Override default encoding if specified by user
|
||||
DEFAULT_ENCODING = args.encoding
|
||||
@@ -1084,17 +1166,20 @@ def main() -> None:
|
||||
)
|
||||
|
||||
LOG.debug(
|
||||
"Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s",
|
||||
*chain(*encodings)
|
||||
"Running with encodings: %s: %s, %s: %s, %s: %s, %s: %s", *chain(*encodings)
|
||||
)
|
||||
|
||||
for stream, encoding in encodings:
|
||||
if encoding.lower() != DEFAULT_ENCODING:
|
||||
LOG.warning("Running with unsupported encoding '%s': %s"
|
||||
" - Things are likely to fail from here onwards", stream, encoding)
|
||||
LOG.warning(
|
||||
"Running with unsupported encoding '%s': %s"
|
||||
" - Things are likely to fail from here onwards",
|
||||
stream,
|
||||
encoding,
|
||||
)
|
||||
|
||||
# Load Mozilla profile and initialize NSS before asking the user for input
|
||||
moz = MozillaInteraction()
|
||||
moz = MozillaInteraction(args.non_fatal_decryption)
|
||||
|
||||
basepath = os.path.expanduser(args.profile)
|
||||
|
||||
|
||||
68
tests/handle_corrupted_passwords.t
Executable file
68
tests/handle_corrupted_passwords.t
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from simpletap.fdecrypt import lib
|
||||
|
||||
|
||||
class TestCorruptedPassword(unittest.TestCase):
|
||||
def validate_one(self, userkey, grepkey, output):
|
||||
expected = lib.get_user_data(userkey)
|
||||
matches = lib.grep(grepkey, output, context=1)
|
||||
|
||||
self.assertEqual(matches, expected)
|
||||
|
||||
def validate(self, out):
|
||||
self.validate_one("decryption_failed", "Username: .* decryption failed", out)
|
||||
self.validate_one("doesntexist", "doesntexist", out)
|
||||
self.validate_one("onemore", "onemore", out)
|
||||
self.validate_one("complex", "cömplex", out)
|
||||
self.validate_one("jamie", "jãmïe", out)
|
||||
|
||||
def validate_exception(self, out):
|
||||
# error is "ValueError: Username/Password decryption (...) Credentials damaged (...)"
|
||||
err = "Credentials damaged or cert/key file mismatch."
|
||||
match = lib.grep(err, out)
|
||||
self.assertIn("ValueError: Username/Password", match)
|
||||
|
||||
def validate_error(self, out):
|
||||
# error is "ERROR - Username/Password decryption (...) Credentials damaged (...)"
|
||||
err = "Credentials damaged or cert/key file mismatch."
|
||||
match = lib.grep(err, out)
|
||||
self.assertIn("ERROR - Username/Password", match)
|
||||
|
||||
def run_firefox_nopassword(self, cmd):
|
||||
output = lib.run(cmd)
|
||||
self.validate(output)
|
||||
self.validate_exception(output)
|
||||
|
||||
def run_firefox_nopassword_error(self, cmd):
|
||||
# returncode 17 is DECRYPTION_FAILED
|
||||
output = lib.run_error(cmd, returncode=17)
|
||||
self.validate_error(output)
|
||||
|
||||
def test_corrupted_skip_firefox_114(self):
|
||||
self.test = os.path.join(lib.get_test_data(),
|
||||
"test_profile_firefox_nopassword_114")
|
||||
|
||||
# Must run in non-interactive mode or password prompt will be shown
|
||||
cmd = lib.get_script() + [self.test, "-n", "--non-fatal-decryption"]
|
||||
|
||||
self.run_firefox_nopassword(cmd)
|
||||
|
||||
def test_corrupted_firefox_114(self):
|
||||
self.test = os.path.join(lib.get_test_data(),
|
||||
"test_profile_firefox_nopassword_114")
|
||||
|
||||
# Must run in non-interactive mode or password prompt will be shown
|
||||
cmd = lib.get_script() + [self.test, "-n"]
|
||||
|
||||
self.run_firefox_nopassword_error(cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from simpletap import TAPTestRunner
|
||||
unittest.main(testRunner=TAPTestRunner(buffer=True), exit=False)
|
||||
|
||||
# vim: ai sts=4 et sw=4
|
||||
BIN
tests/test_data/test_profile_firefox_nopassword_114/cert9.db
Normal file
BIN
tests/test_data/test_profile_firefox_nopassword_114/cert9.db
Normal file
Binary file not shown.
BIN
tests/test_data/test_profile_firefox_nopassword_114/key4.db
Normal file
BIN
tests/test_data/test_profile_firefox_nopassword_114/key4.db
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"nextId":6,"logins":[{"id":1,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECEgmzKOCavAWBAiab7yWG12/Rw==","encryptedPassword":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECIm+i6iNZ4okBAjDGi5OTiNc9Q==","guid":"{000011d3-d88f-4d72-916e-46aba374ee68}","encType":1,"timeCreated":1688676766618,"timeLastUsed":1688677135670,"timePasswordChanged":1688676766618,"timesUsed":2},{"id":2,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECMmE70ZVP0E4BBCJNgP9zigxQG7wA5Xtf6J/","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECAaKjRlvw62zBBDn4tthVPtEKCqkeOeUQZaM","guid":"{dd7a17bb-14c8-46e2-8170-f09cb35a6c01}","encType":1,"timeCreated":1688677150303,"timeLastUsed":1688677150303,"timePasswordChanged":1688677150303,"timesUsed":1},{"id":3,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECCb0mK51cDIgBAjToSpG3VVLnQ==","encryptedPassword":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECB3Oul6W4qDQBBB917xhQ/IHXUnctPwKU6vY","guid":"{badd0e1b-c526-4548-a79b-0dd878389276}","encType":1,"timeCreated":1688677214767,"timeLastUsed":1688677214767,"timePasswordChanged":1688677214767,"timesUsed":1},{"id":4,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECPWy/Wb/zVxsBBCHZJUabIuDWSSPh7IvF4M/","encryptedPassword":"MFIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBWgpjB1kLSYBCj+3VaCZAPhdWXO40qlQTBj7wzjrAVOCGnug8366ez2dmn4/TpGPxyi","guid":"{ccd01360-2a8c-46d3-993c-5aee0dbbf647}","encType":1,"timeCreated":1688677292142,"timeLastUsed":1688677292142,"timePasswordChanged":1688677292142,"timesUsed":1},{"id":5,"hostname":"https://github.com","httpRealm":null,"formSubmitURL":"https://github.com","usernameField":"login","passwordField":"password","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECOcZUkTRCDK5BAjDFjezUYVSFg==","encryptedPassword":"MFoEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECM8rG1/2s/7uBDDLU1D80YJO5o2tkjh/WaXUbhOWNgLiCvPaaOFQfaI4/iCaS70SD4UrEXJSBm7ph7o=","guid":"{80816ccc-3b32-4125-89fd-36c388d91ebc}","encType":1,"timeCreated":1688677398674,"timeLastUsed":1688677398674,"timePasswordChanged":1688677398674,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":3}
|
||||
3
tests/test_data/users/decryption_failed.user
Normal file
3
tests/test_data/users/decryption_failed.user
Normal file
@@ -0,0 +1,3 @@
|
||||
Website: https://github.com
|
||||
Username: '*** decryption failed ***'
|
||||
Password: '*** decryption failed ***'
|
||||
Reference in New Issue
Block a user