Last active
April 14, 2020 20:25
-
-
Save micksmix/bd6e6acec9a83da22fdf050f55003b7d to your computer and use it in GitHub Desktop.
Move *some* LastPass data into Firefox Lockwise
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python2 | |
| # NOTE: This requires Python2 | |
| # | |
| # I forked this gist, and modified it to be able to | |
| # parse a LastPass CSV export and import it into | |
| # Firefox Lockwise | |
| # https://gist.github.com/rfk/916d9ca684f862b1c1030c685a5a4d19 | |
| # | |
| # Use this script like so: | |
| # | |
| # $> pip install PyFxA syncclient cryptography pathlib2 | |
| # $> python2 ./lastpass2fflockwise.py /path/to/lastpass_export.csv | |
| # | |
| # It will prompt for your Firefox Account email address and | |
| # password, parse the LastPass CSV export, and add them 1 by 1 to | |
| # Firefox Lockwise, then sync locally | |
| # | |
| # There may be bugs. I used it myself, and it worked. YMMV | |
| # This does NOT import secure notes. It only reads and uploads: | |
| # * username | |
| # * password | |
| # * url | |
| # | |
| # The time created / time changed values are all set to the time when this | |
| # script is run. | |
| # | |
| # Here is how to export LastPass secrets to a CSV: | |
| # https://support.logmeininc.com/lastpass/help/export-your-passwords-and-secure-notes-lp040004 | |
| # | |
| import os | |
| import time | |
| import json | |
| import getpass | |
| import hmac | |
| import hashlib | |
| import base64 | |
| import uuid | |
| import csv | |
| from pathlib2 import Path | |
| from binascii import hexlify | |
| from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
| from cryptography.hazmat.primitives import padding | |
| from cryptography.hazmat.backends import default_backend | |
| import fxa.core | |
| import fxa.crypto | |
| import syncclient.client | |
| CRYPTO_BACKEND = default_backend() | |
| EMAIL = "" #raw_input("Email: ") | |
| PASSWORD = "" #getpass.getpass("Password: ") | |
| # Below here is all the mechanics of uploading them to the sync server. | |
| def main(csv_filename): | |
| creds = login() | |
| upload_real_password_records(csv_filename, *creds) | |
| def login(): | |
| client = fxa.core.Client() | |
| print "Signing in as", EMAIL, "..." | |
| session = client.login(EMAIL, PASSWORD, keys=True) | |
| try: | |
| status = session.get_email_status() | |
| while not status["verified"]: | |
| print "Please click through the confirmation email." | |
| if raw_input("Hit enter when done, or type 'resend':").strip() == "resend": | |
| session.resend_email_code() | |
| status = session.get_email_status() | |
| assertion = session.get_identity_assertion("https://token.services.mozilla.com/") | |
| _, kB = session.fetch_keys() | |
| finally: | |
| session.destroy_session() | |
| return assertion, kB | |
| def upload_real_password_records(csv_filename, assertion, kB): | |
| # Connect to sync. | |
| xcs = hexlify(hashlib.sha256(kB).digest()[:16]) | |
| client = syncclient.client.SyncClient(assertion, xcs) | |
| # Fetch /crypto/keys. | |
| raw_sync_key = fxa.crypto.derive_key(kB, "oldsync", 64) | |
| root_key_bundle = KeyBundle( | |
| raw_sync_key[:32], | |
| raw_sync_key[32:], | |
| ) | |
| keys_bso = client.get_record("crypto", "keys") | |
| keys = root_key_bundle.decrypt_bso(keys_bso) | |
| default_key_bundle = KeyBundle( | |
| base64.b64decode(keys["default"][0]), | |
| base64.b64decode(keys["default"][1]), | |
| ) | |
| # Make a lot of password records. | |
| #url,username,password,extra,name,grouping,fav | |
| with open(csv_filename) as csv_file: | |
| csv_reader = csv.DictReader(csv_file) | |
| for line in csv_reader: | |
| url = line["url"] | |
| username = line["username"] | |
| password = line["password"] | |
| name = line["name"] | |
| now = int(time.time() * 1000) | |
| r = { | |
| "id": "{%s}" % (uuid.uuid4(),), | |
| "username": username, | |
| "password": password, | |
| "hostname": url, | |
| "formSubmitURL": "", | |
| "timeCreated": now, | |
| "timePasswordChanged": now, | |
| "httpRealm": None, | |
| } | |
| er = default_key_bundle.encrypt_bso(r) | |
| client.put_record("passwords", er) | |
| print " wrote credentials for: (%s)" % url | |
| # print "Synced password records:" | |
| # for er in client.get_records("passwords"): | |
| # r = default_key_bundle.decrypt_bso(er) | |
| print "Done!" | |
| class KeyBundle: | |
| """A little helper class to hold a sync key bundle.""" | |
| def __init__(self, enc_key, mac_key): | |
| self.enc_key = enc_key | |
| self.mac_key = mac_key | |
| def decrypt_bso(self, data): | |
| payload = json.loads(data["payload"]) | |
| mac = hmac.new(self.mac_key, payload["ciphertext"], hashlib.sha256) | |
| if mac.hexdigest() != payload["hmac"]: | |
| raise ValueError("hmac mismatch: %r != %r" % (mac.hexdigest(), payload["hmac"])) | |
| iv = base64.b64decode(payload["IV"]) | |
| cipher = Cipher( | |
| algorithms.AES(self.enc_key), | |
| modes.CBC(iv), | |
| backend=CRYPTO_BACKEND | |
| ) | |
| decryptor = cipher.decryptor() | |
| plaintext = decryptor.update(base64.b64decode(payload["ciphertext"])) | |
| plaintext += decryptor.finalize() | |
| unpadder = padding.PKCS7(128).unpadder() | |
| plaintext = unpadder.update(plaintext) + unpadder.finalize() | |
| return json.loads(plaintext) | |
| def encrypt_bso(self, data): | |
| plaintext = json.dumps(data) | |
| padder = padding.PKCS7(128).padder() | |
| plaintext = padder.update(plaintext) + padder.finalize() | |
| iv = os.urandom(16) | |
| cipher = Cipher( | |
| algorithms.AES(self.enc_key), | |
| modes.CBC(iv), | |
| backend=CRYPTO_BACKEND | |
| ) | |
| encryptor = cipher.encryptor() | |
| ciphertext = encryptor.update(plaintext) | |
| ciphertext += encryptor.finalize() | |
| b64_ciphertext = base64.b64encode(ciphertext) | |
| mac = hmac.new(self.mac_key, b64_ciphertext, hashlib.sha256).hexdigest() | |
| return { | |
| "id": data["id"], | |
| "payload": json.dumps({ | |
| "ciphertext": b64_ciphertext, | |
| "IV": base64.b64encode(iv), | |
| "hmac": mac, | |
| }) | |
| } | |
| if __name__ == "__main__": | |
| import sys | |
| if len(sys.argv) > 1: | |
| csv_filename = sys.argv[1] | |
| else: | |
| print "[!] You must provide a path to a CSV file as only argument!" | |
| exit(1) | |
| EMAIL = raw_input("Email: ") | |
| PASSWORD = getpass.getpass("Password: ") | |
| my_file = Path(csv_filename) | |
| if my_file.is_file(): | |
| main(csv_filename) | |
| else: | |
| print "[!] You must provide a path to a valid CSV file as only argument!" | |
| exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note that this means you may not get proper breached login warnings.
If you make this
"formSubmitURL": "",then it will match on any form action for a page.Does LastPass not distinguish HTTP auth logins vs. form logins? If so, this should be the HTTP realm and formSubmitURL should be None for HTTP auth logins.
You will also want to ensure that
hostnameandformSubmitURLare origins when non-null/empty (e.g. https://accounts.google.com) and don't include a path segment for things to work properly.