Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: node script for decrypting bookmarks #105

Closed
abudden opened this issue Jun 25, 2019 · 6 comments
Closed

Feature Request: node script for decrypting bookmarks #105

abudden opened this issue Jun 25, 2019 · 6 comments

Comments

@abudden
Copy link

abudden commented Jun 25, 2019

I wasn't sure whether this should be raised on the API or APP issue page; apologies if I've judged it wrong.

I use the Chrome extension to back up to my home server. At the moment, I'm keeping historical backups (so if I ever accidentally delete something I can plausibly go in and hack the database to load an old version) using a simple script:

docker exec -t $(docker-compose ps -q db) mongoexport \
        -u "${XBS_DB_USERNAME}" \
        -p "${XBS_DB_PASSWORD}" \
        --authenticationDatabase admin \
        -d xbrowsersync \
        -c bookmarks \
        --jsonArray \
        | grep '^\[' \
        > export.json

It would be really useful to actually be able to read the bookmarks from a script on the server (I could then implement things like backups of the actual bookmarked websites, see what has actually changed in the bookmark history etc).

The problem is: the bookmarks are encrypted. This is a good thing, but I know the sync id and password and I would love to have a script that took export.json from the above script (or just used the REST api) along with the sync id and password and spat out the decrypted bookmarks. I can do this manually with the backup feature, but I want to stick this in a cron job and have it run on the server regularly.

I had a go experimenting with a copy of bits of the app source code and node.js, but kept getting pages of error messages; my javascript knowledge isn't up to resolving all of them sadly.

I've raised this on the app page as that's where most of the decryption code exists. I'm hoping that someone good at javascript could somehow import utility.js and write a few short lines of code to make it work.

@nero120
Copy link
Member

nero120 commented Jun 25, 2019

Hi @abudden , I don't have time to look at this right now, but it shouldn't be too difficult to adapt existing code into a Node script. I can give you the gist then yourself or someone else could give it a go.

The functions required to decrypt bookmarks data are:

  • utility.getPasswordHash: Before decrypting, the plain text password needs to be converted to a hash. The salt for the hash is the Sync ID, the key derivation function used is PBKDF2 with SHA-256 as the hashing function applied for 250,000 iterations. Once you have generated this hash, it should match the password value stored in the extensions local storage (when synced, run chrome.storage.local.get('password', x => console.log(x.password)) in chrome extension background to get this value).
  • utility.decryptData_v2: Once the password hash has been generated, convert the encrypted bookmarks data to a byte array and grab the first 16 bytes from the array to get the initialization vector (iv). The rest of the byte array (minus the first 16 iv bytes) is the data that you will decrypt using AES-GCM, the iv and the password hash to convert into json bookmarks data.

xBrowserSync uses the browser's implementation of the Web Crypto API, unfortunately Node does not yet include such an implementation, but the Node crypto module should provide similar functions (crypto.pbkdf2 and crypto.createDecipheriv respectively).

It might look scary but it's less than 100 lines of code combined! Definitely doable. Give it a go and see how you get on.

@abudden
Copy link
Author

abudden commented Jun 26, 2019

@nero120 Thanks for the notes. I've had another go (starting from scratch rather than my previous attempt with copying and pasting the xbrowsersync source code). As a javascript novice, I still feel like I'm fumbling in the dark, but I've included my first attempt below.

It looked promising to me, but there are at least two major issues:

  • The key generation produces a different result to the one that's stored in localStorage['xBrowserSync-password'].
  • The createDecipheriv function is complaining about an Invalid key length.

Do you have time to have a look through what I've tried to see if you can spot the problems? This is literally my first ever time using node, so I doubt I've written the code in anywhere near the best way...

const crypto = require('crypto');
const util = require('util');

// The raw entered password (50 characters in my case)
var PASSWORD = '';

// The sync ID extracted from the xbrowsersync UI (32 characters)
var SYNC_ID = '';

// The hashed password, extracted with Chrome
// console (44 characters, base64):
//
// > console.log(localStorage['xBrowserSync-password']);
//
// NOTE that I couldn't get chrome.storage.local.get to work
// in the extension background inspect or popup inspect: in
// both cases it said that chrome.storage is undefined.
var HASHED_PASSWORD = '';

// Encrypted bookmarks, extracted for testing from Chrome
// console (44296 characters in my case, base64):
//
// > console.log(localStorage['xBrowserSync-cachedBookmarks']);
//
var ENCRYPTED_BOOKMARKS = '';

const pbkdf2 = util.promisify(crypto.pbkdf2);

function getPasswordHash(password, syncId) {
    var encoder = new util.TextEncoder('utf-8');
    var keyData = encoder.encode(password);
    var salt = encoder.encode(syncId);

    // Key length of 32 was chosen as it seems to produce a result
    // that matches (in length, if not in value) the hashed password
    // from Chrome localStorage.
    return pbkdf2(keyData, salt, 250000, 32, 'sha256')
        .then(function(derivedKey) {
            return derivedKey.toString('base64');
        });
}

var encrypted_bytes = Buffer.from(ENCRYPTED_BOOKMARKS, 'base64');

var iv = encrypted_bytes.slice(0, 16);
var enc = encrypted_bytes.slice(16);

getPasswordHash(PASSWORD, SYNC_ID)
    .then((key) => {
        if (key.toString('base64') !== HASHED_PASSWORD) {
            // ERROR: This shouldn't print out, but it does
            console.log('Hashed password seems wrong: ', key.toString('base64'));
        }

        // ERROR Produces UnhandledPromiseRejectionWarning: Error: Invalid key length
        var decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
        // 'hex' parameter is just a guess...
        var decrypted = decipher.update(enc, 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        return decrypted;
    }).then((decrypted) => {
        // TODO: Write the decrypted data to a file...
        console.log(decrypted);
    });

@nero120
Copy link
Member

nero120 commented Jun 26, 2019

Most of that looks fine, I've had a play and made a few small changes. However my version fails due to a wierd Node crypto behaviour, I've logged an issue with them so let's see what they say...

@nero120
Copy link
Member

nero120 commented Jun 27, 2019

Working version: https://gist.github.com/nero120/e878e40b14655c9526680472376b4f8c

You'll need to run npm i after downloading the files in order to install the dependencies. I forgot to mention that the data needs to be decompressed using lzutf8 after decryption, I've added this and then beautified the output so it appears more readable that just a big blob of json (feel free to change if you prefer not to beautify).

Hope that helps!

@nero120 nero120 closed this as completed Jun 27, 2019
@abudden
Copy link
Author

abudden commented Jun 27, 2019

Fantastic, thanks @nero120

I've implemented it on my server and it works like a dream.

@nero120
Copy link
Member

nero120 commented Jun 27, 2019

Glad to hear it! 😄

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

No branches or pull requests

2 participants