Node.js client for Firefox Sync.
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!
npm install firefox-sync
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. |
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
}
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
.
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
}
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"
}
}
]
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"
}
}
There's a number of endpoints that return some information about this Firefox Sync instance.
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
]
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
}
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
}
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
}
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:
- A journey to scripting Firefox Sync / Lockwise: existing clients
- A journey to scripting Firefox Sync / Lockwise: figuring the protocol
- A journey to scripting Firefox Sync / Lockwise: understanding BrowserID
- A journey to scripting Firefox Sync / Lockwise: hybrid OAuth
- A journey to scripting Firefox Sync / Lockwise: complete OAuth