Seeding LastPass from Google Chrome

I've tried LastPass before, but couldn't quite get the hang of it. I found it fairly frustrating as well that it prompted me all the time: "Do you want LastPass to remember this information? I know you're not really thinking about me right now, but I really need you to take some time for little old LastPass here. Now."

But at my new job we use LastPass pretty heavily, and it turns out that if you stick with it and dote on it for a while, it's pretty useful and eventually unintrusive. So I'm going to take another shot. I bought a Premium subscription. We'll see how it goes.

No starting over

There's one thing I didn't want to do: gradually load LastPass up again, one needy password prompt at a time. But I've been allowing Chrome to store my passwords, and I figured there must be a way to get the passwords out of it. Chrome Settings offers a view where you can see one password at a time, but there's no bulk export, so that's a lot of copy pasta. It was up to the Internet to find a solution.

Unfortunately the Internet didn't have one. There was literally nothing out there that my meager Google-fu was able to turn up for decrypting the passwords stored in your ~/Library/Application Support/Google/Chrome/Profile N/Login Data file. LastPass itself has an import from Google Chrome option in its extension, but I couldn't make it work even after installing the binary helper program for the extension.

However, Nate Henrie described the Chrome decryption algorithm used for cookies, and a project called hindsight provided a comprehensive framework for it. It turns out the same encryption is used for passwords as for cookies, sensibly enough. I extended hindsight to support login data, and it's been merged upstream, so now you can do this too.

Extracting login data from Chrome

Using this is pretty simple now. Note that you need Python, and on MacOS and Linux you need the keyring module.

$ sudo pip install keyring
$ git clone https://github.com/obsidianforensics/hindsight
$ cd hindsight
$ python ./hindsight.py -i ~/Library/Application\ Support/Google/Chrome/Default -f json -d mac
$ vi ~/Hindsight\ Internet\ History\ Analysis\ \(2016-02-16T23-31-27\) 

(The -i argument is correct for your default profile on MacOS. I used -d mac since I'm using a Mac. YMMV on other platforms or profiles, but with some option twiddling the command should work.)

The resulting file, Hindsight Internet History Analysis..., is a JSON file containing all the details of your session — including decrypted users, passwords, and URLs. Here's an example from my results (values changed, obviously).

    ...
    {
        "count": 0,
        "interpretation": null,
        "name": "",
        "url": "https://www.ohloh.net/sessions",
        "timestamp": "2011-01-22T00:27:55+00:00",
        "row_type": "login (username)",
        "value": "dgc",
        "date_created": "2011-01-22T00:27:55+00:00"
    },
    {
        "count": 0,
        "interpretation": null,
        "name": "",
        "url": "https://www.ohloh.net/sessions",
        "timestamp": "2011-01-22T00:27:55+00:00",
        "row_type": "login (password)",
        "value": "MYPASSWORD",
        "date_created": "2011-01-22T00:27:55+00:00"
    },
    ...

Parsing the JSON

To import that into LastPass, we need to turn it into a CSV — something like this:

url,username,password,name,extra
https://www.ohloh.net/sessions,dgc,MYPASSWORD,ohloh.net,"Created 2011-01-22T00:27:55+00:00
Imported from Google Chrome 2016-02-23T00:11:41+00:00"

Note: In a twist on conventional CSV, LastPass wants line breaks in the "extra" field to have actual line breaks in the CSV! Its CSV parser gives quotations primacy over record separators. It parses sequentially, and if a newline appears within balanced double-quotes it is implicitly escaped.

It would have been pretty straightforward to write a Python program to parse the JSON and give me something usable, but I decided instead to take ten times as long and figure out how to do it in the utterly brilliant jq. I've been a fan for a few years but have never had a truly challenging task to accomplish with jq. As a learning experience this was excellent.

Here's the jq filter I came up with:

["url", "username", "password", "name", "extra"],
([
    .parsed_artifacts[]
    | select(has("row_type"))
    | select(.row_type == "login (username)" or
             .row_type == "login (password)")
    | { url: .url, (.row_type[7:11]): .value, ate: .date_created }
]
| group_by(.url)
| .[]
| [
    .[0].url,
    (.[0].user // .[1].user),
    (.[0].pass // .[1].pass),
    (.[0].url / "/")[2],
    "Created at " + .[0].date + "\nImported from Google Chrome " +
        (now | todateiso8601)
])
| @csv

I saved this script in a file called hindsight2csv, then ran:

$ jq -r -f hindsight2csv <~/Hindsight\ Internet* >lastpass.csv

Then I imported lastpass.csv at https://lastpass.com/import.php. 161 passwords transferred from Chrome to LastPass; voilà.

Wait, what?

If you're curious what all that jq filter business actually means, here's a rundown.

  1. First, make an array of static strings: url, username, etc. These will be used as column headers once we get to some CSV output.
  2. Then step through all the items in the parsed_artifacts element of the JSON output. parsed_artifacts is where cookies, bookmarks, and login data (usernames and passwords) are all stored. From among all these:
  3. Select only those items which have a row_type key;
  4. and those items whose row_type is either login (username) or login (password);
  5. creating an object for each match holding the url, the username or password, and the date of the entry in the Chrome login data file.
  6. Store all that into an array of JSON objects.
  7. Then rearrange this array to group objects with common urls into sub-arrays.
  8. For each grouping, we now have multiple objects — one for the username and one for the password — which share a URL. In each case the username object can come before or after the password object — they are unordered. So create another array with the url, the username, the password, the site hostname, and a description ("extra").
  9. The url is copied directly out of the first object.
  10. The username may appear in either the first or second object, so pick whichever one has it using the // operator.
  11. Same for the password.
  12. For the site hostname, split the url on / and pick the third item in the split.
  13. For the extra field, compose some static text using the date from the Hindsight data and the current date/time generated internally by jq.
  14. At this point, jq internally has a list of arrays. Feeding into the @csv filter rewrites this as a bunch of comma-separated values on distinct lines. The -r option to jq ensures that string values are not extra-quoted on output.