Implement non-fatal password decrypting

Fixes #97
This commit is contained in:
Renato Alves
2023-07-26 22:18:03 +02:00
parent 2c61b27e67
commit 66c35a0531
8 changed files with 291 additions and 115 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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

View File

@@ -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}

View File

@@ -0,0 +1,3 @@
Website: https://github.com
Username: '*** decryption failed ***'
Password: '*** decryption failed ***'