@steven-sg/firefox-sync
TypeScript icon, indicating that this package has built-in type declarations

1.2.2 • Public • Published

Node Firefox Sync npm version

Node.js client for Firefox Sync.

Overview

I actually wanted to inspect my raw Firefox Sync data for some reason that day. I was lazy to put together and run the example code from the blog posts I wrote earlier this year, so I ended up spending a few days to release a Node.js client and CLI instead. Go figure out.

You can authenticate to Firefox Sync through password authentication or through OAuth. With OAuth, the user password is never exposed to the program so I would highly recommend this method for security reasons.

Then you can query all the Firefox Sync collections, as well as the other information endpoints that are available.

The main things that are missing are:

  • OAuth token refresh: while this library will automatically refresh the Firefox Sync token that expires every hour, it doesn't refresh the OAuth token that expires every day.
  • Write methods: it's currently a read-only library and doesn't support creating, updating or deleting entries. That said the encryption method is already implemented so it's just a matter of calling the right endpoint.

Feel free to open a pull request to add those if you need them!

Installation

npm install firefox-sync

Usage

const Sync = require('firefox-sync')
const sync = Sync(options)

The options object can contain:

Name Description
credsFile Manage authentication state in the given file. Useful if you want to keep state between multiple command invocations (e.g. a CLI).
clientId OAuth client ID. Defaults to the Android app's one for convenience.
scope OAuth scope to access Firefox Sync, you probably don't want to change the default as it's currently the only scope that gives access to Sync data.
authServerUrl Only used for password authentication, Firefox Accounts API endpoint, in case you want to use a custom server.
authorizationUrl OAuth authorization URL. Default from the OpenID configuration.
tokenEndpoint OAuth token endpoint. Default from the OpenID configuration.
tokenServerUrl TokenServer URL, in case you want to use a custom server.
oauthOptions Extra OAuth parameters. You'll mainly want to use this to pass access_type: 'offline' to get a refresh token.

Authentication

Email and password

Warning: while this is probably the easiest method to sign in, it gives access to the program to the plaintext password. Even though Mozilla's authentication mechanism never sends the password over the network (on top of being TLS encrypted), it's still going to be stored in RAM and JavaScript doesn't give us a way to reliably wipe it after authenticating. Keep that in mind when evaluating your threat model.

const creds = await sync.auth.password('hello@mozilla.com', 'The password goes here!')

const creds = await sync.auth.password('hello@mozilla.com', 'The password goes here!', {
  authServerUrl: 'https://your.custom.url/'
})
Response
{
  "oauthToken": {
    "access_token": "32 bytes of hex",
    "token_type": "bearer",
    "scope": "https://identity.mozilla.com/apps/oldsync",
    "expires_in": 86399,
    "auth_at": 1634346661,
    "refresh_token": "32 bytes of hex"
  },
  "syncKeyBundle": {
    "encryptionKey": "32 bytes of Base64",
    "hmacKey": "32 bytes of Base64",
    "kid": "A timestamp and 16 bytes of Base64URL"
  },
  "token": {
    "id": "A bunch of Base64URL",
    "key": "32 bytes of Base64URL",
    "uid": 999999999,
    "api_endpoint": "https://sync-1-us-west1-g.sync.services.mozilla.com/1.5/999999999",
    "duration": 3600,
    "hashalg": "sha256",
    "hashed_fxa_uid": "16 bytes of hex",
    "node_type": "spanner"
  },
  "tokenIssuedAt": 1634346661940
}

OAuth

First, we issue a OAuth challenge that the user needs to open in a browser.

If you don't have your own OAuth client ID with a properly configured redirect URL for your application, you can use a public client ID like the one of the Android app, but you'll need to use web debugging tools to retrieve the OAuth response code, so this will only work for testing purpose.

const challenge = await sync.auth.oauth.challenge()

const challenge = await sync.auth.oauth.challenge({
  oauthOptions: {
    access_type: 'offline'
  }
})
Response
{
  "keyPair": "`KeyPairKeyObjectResult` for internal use",
  "state": "16 bytes of Base64URL",
  "codeVerifier": "32 bytes of Base64URL",
  "url": "https://accounts.firefox.com/authorization?all-the-challenge-params-go-here"
}

Upon successful authentication, the user is redirected to the configured URL that will include a code and state query string parameters that you need to pass to the auth.oauth.complete function.

const result = {
  code: '32 bytes of hex',
  state: '16 bytes of Base64URL (ideally the same as the challenge)'
}

const creds = await sync.auth.oauth.complete(challenge, result)

const creds = await sync.auth.oauth.complete(challenge, result, {
  tokenEndpoint: 'https://your.custom.url/token'
})

Same output as auth.password.

Collections

getCollections

Returns an object mapping collection names associated with the account to the last modified time for each collection.

const collections = await sync.getCollections()
Response
{
  "passwords": 1634346661.94,
  "bookmarks": 1634346661.94,
  "crypto": 1634346661.94,
  "prefs": 1634346661.94,
  "meta": 1634346661.94,
  "addons": 1634346661.94,
  "tabs": 1634346661.94,
  "clients": 1634346661.94,
  "history": 1634346661.94,
  "forms": 1634346661.94
}

getCollection

By default only the BSO IDs are returned, but full objects can be requested using the full parameter. If the collection does not exist, an empty list is returned.

const items = await sync.getCollection('bookmarks')
Response
[
  "foo",
  "bar",
  "baz"
]
const items = await sync.getCollection('bookmarks', { full: true })
const items = await sync.getCollection('bookmarks', { full: true, ids: ['foo', 'bar'] })
Response
[
  {
    "bso": {
      "id": "foo",
      "modified": 1634346661.94,
      "payload": "{\"encrypted\":\"stuff\"}"
    },
    "payload": {
      "decrypted": "stuff"
    }
  }
]

getCollectionItem

Returns the BSO in the collection corresponding to the requested ID.

const item = await sync.getCollectionItem('bookmarks', 'foo')
Response
{
  "bso": {
    "id": "foo",
    "modified": 1634346661.94,
    "payload": "{\"encrypted\":\"stuff\"}"
  },
  "payload": {
    "decrypted": "stuff"
  }
}

Information

There's a number of endpoints that return some information about this Firefox Sync instance.

getQuota

Returns a two-item list giving the user’s current usage and quota (in kB). The second item will be null if the server does not enforce quotas.

const quota = await sync.getQuota()
Response
[
  69.133742,
  null
]

getCollectionUsage

Returns an object mapping collection names associated with the account to the data volume used for each collection (in kB).

const usage = await sync.getCollectionUsage()
Response
{
  "addons": 0.7588336369,
  "crypto": 0.5156744894,
  "forms": 0.3097969336,
  "tabs": 0.2830539361,
  "bookmarks": 0.6618207313,
  "clients": 0.9727294557,
  "prefs": 0.3751385437,
  "meta": 0.6064291011,
  "passwords": 0.7713613800,
  "history": 0.9888805912
}

getCollectionCounts

Returns an object mapping collection names associated with the account to the total number of items in each collection.

const usage = await sync.getCollectionCounts()
Response
{
  "history": 69,
  "addons": 1,
  "forms": 42,
  "meta": 1,
  "bookmarks": 1337,
  "tabs": 1,
  "prefs": 1,
  "crypto": 1,
  "passwords": 420,
  "clients": 1
}

getConfiguration

Provides information about the configuration of this storage server with respect to various protocol and size limits.

const usage = await sync.getConfiguration()
Response
{
  "max_post_bytes": 2097152,
  "max_post_records": 100,
  "max_record_payload_bytes": 2097152,
  "max_request_bytes": 2101248,
  "max_total_bytes": 100000000,
  "max_total_records": 1664,
  "max_quota_limit": 2097152000
}

See also

Firefox Sync CLI, a command line interface to access your Sync data.

The story on how this all started when I tried to access my Lockwise passwords from the CLI:

  1. A journey to scripting Firefox Sync / Lockwise: existing clients
  2. A journey to scripting Firefox Sync / Lockwise: figuring the protocol
  3. A journey to scripting Firefox Sync / Lockwise: understanding BrowserID
  4. A journey to scripting Firefox Sync / Lockwise: hybrid OAuth
  5. A journey to scripting Firefox Sync / Lockwise: complete OAuth

Readme

Keywords

none

Package Sidebar

Install

npm i @steven-sg/firefox-sync

Weekly Downloads

0

Version

1.2.2

License

Unlicense

Unpacked Size

44.2 kB

Total Files

24

Last publish

Collaborators

  • steven-sg