Skip to content

Instantly share code, notes, and snippets.

@micksmix
Last active April 14, 2020 20:25
Show Gist options
  • Select an option

  • Save micksmix/bd6e6acec9a83da22fdf050f55003b7d to your computer and use it in GitHub Desktop.

Select an option

Save micksmix/bd6e6acec9a83da22fdf050f55003b7d to your computer and use it in GitHub Desktop.
Move *some* LastPass data into Firefox Lockwise
#!/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)
@mnoorenberghe
Copy link

mnoorenberghe commented Apr 14, 2020

The time created / time changed values are all set to the time when this script is run.

Note that this means you may not get proper breached login warnings.

"formSubmitURL": url,

If you make this "formSubmitURL": "", then it will match on any form action for a page.

"httpRealm": None,

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 hostname and formSubmitURL are origins when non-null/empty (e.g. https://accounts.google.com) and don't include a path segment for things to work properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment