A journey to scripting Firefox Sync / Lockwise: hybrid OAuth

Impersonating the Android app to replace deprecated BrowserID with OAuth

August 8, 2021

Picture credit: Val

This article is part of a series about scripting Firefox Sync / Lockwise.

  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

Welcome to the last post of this series about scripting Firefox Sync! So far weā€™ve managed to run the Sync clients we found in the wild (dating from 8 years ago), and taking inspiration from them, plus all the available documentation online, we built our own client, which required us to deconstruct the BrowserID protocol.

And while I was pretty satisfied with this, there was still one little thing bugging me.

See, while I was reading everything possible online about the Firefox Accounts, Firefox Sync and BrowserID protocols in order to make this work, including the code of the production clients and servers involved, I stumbled upon this comment in the Firefox Accounts server /certificate/sign endpoint, that we use to sign a BrowserID public key and get back a certificate:

// This is a legacy endpoint that's typically only used by clients
// connected to Sync, so assume `service=sync` for metrics logging
// purposes unless we're told otherwise.
// This is a legacy endpoint that's typically only used by clients
// connected to Sync.
// This is a legacy endpoint.

I donā€™t like the idea of using a legacy endpoint when writing new code. There must be something better.

Exploring OAuth

While the TokenServer documents OAuth as an alternative to BrowserID to get credentials, itā€™s unclear how to use it. All that page says is ā€œthe client must obtain an OAuth access token and the corresponding encryption key as a JWKā€, but doesnā€™t mention where to get the OAuth token and the corresponding key.

The BrowserID instructions werenā€™t necessarily clearer, but at least it had the competitive advantage of having multiple working implementations in the wild that made it easier to understand how it works. OAuth was a different kind of beast.

Sending emails? How about no

The Firefox Ecosystem Platform documents how to integrate with Firefox Accounts using OAuth, but the first thing we can read there is:

Before starting integration, please send a request to fxa-staff[at]mozilla.com to request a short meeting so we can all document our expectations and timelines.

Follow a bit later by:

Register for staging OAuth credentials by filing a deployment bug.

The last thing I want to do is send an email to Mozilla to document my expectations, and file a bug to get credentials. I just want to programmatically access my Firefox Sync data!

By looking up fxa browserid oauth on Google, one of the countless searches I made to try and understand whatā€™s going on, I found this document, which states a couple more things.

All new relying services should integrate with Firefox Accounts via the OAuth 2.0 API. There is also a legacy API based on the BrowserID protocol, which is available only in some Firefox user agents and is not recommended for new applications.

This confirms what I had only read in a code comment on the Firefox Accounts server so far. The BrowserID protocol is indeed deprecated, and itā€™s not just the browserid-crypto package thatā€™s unmaintained as I initially thought.

The OAuth 2.0 API is the preferred method of integrating with Firefox Accounts. To delegate authentication to Firefox Accounts in this manner, you will first need to register for OAuth relier credentials, then add support for a HTTP redirection-based login flow to your service.

Firefox Accounts integration is currently recommended only for Mozilla-hosted services. We are exploring the possibility of allowing non-Mozilla services to delegated authentication to Firefox Accounts, and would welcome discussion of potential use cases on the mailing list.

This matches the documentation I found earlier about the fact that we need to contact Mozilla in order to register for OAuth credentials. At that point it seems like a dead end, and Iā€™m considering to build my client on top of the legacy BrowserID API, since it still works after all, and I spent so much time to understand it in depth anyways.

Circling back

Going back to the OAuth 2.0 API link from the former quote, this points to a page in an archived repo on Github, mozilla/fxa-oauth-server/docs/api.md, itself saying that the page moved to mozilla/fxa-auth-server/fxa-oauth-server/docs/api.md, which is also an archived repo, and with no link this time.

I noticed before that Mozilla archived many of its repos because they moved to a monorepo in mozilla/fxa, which contains the latest version of fxa-auth-server and its API, an API that I had already encountered multiple times since the beginning of this series. Maybe thereā€™s some hope?

We do have a whole OAuth section in there, but I pretty much instantly hit a wall when I see that all those endpoints require an OAuth client ID, which is part of the OAuth credentials Iā€™m supposed to email Mozilla in order to get.

It feels like Iā€™m going in circles. šŸ§

Harvesting a client_id from the Android app

While those endpoints require a client ID, itā€™s not necessary to provide a client secret for public clients (see OAuth grant types). And because the client ID of public clients isā€¦ public, we should be able to easily borrow one from any of the public clients out there (for instance, mobile apps), whether itā€™s from the source code, by decompiling the app, or by inspecting its traffic. And indeed, there is one directly in the lockwise-android repo!

With that client ID in hand, we can start playing with the endpoints we found earlier. At first, Iā€™m thinking that I have to do some kind of OAuth login dance, with the usual redirect URL and code challenge, but it turns out itā€™s not a requirement if we already have a session token, which we do by logging in directly with the userā€™s credentials.

That part was not documented anywhere else, probably because itā€™s not meant to be used by third-party developers like me, but more by the code behind accounts.firefox.com.

The fxa-js-client Iā€™m using even has a method for it, where I can specify a scope of https://identity.mozilla.com/apps/oldsync as documented, and I do get back a valid OAuth token. Sweet.

const scope = 'https://identity.mozilla.com/apps/oldsync'
const oauthToken = await client.createOAuthToken(creds.sessionToken, clientId, { scope })

Unmasking the X-KeyID header

Now thatā€™s not enough to authenticate to the TokenServer, I also need to pass ā€œthe kid field of the encryption key in the X-KeyID headerā€.

This is not really obvious to me because I donā€™t have a kid field in any of the encryption keys I have been manipulating so far. I found out later where that kid should otherwise come from, and let me tell you it was a whole journey of its own. Iā€™ll develop that in the 5th post of this series. Wait, didnā€™t I say earlier that this one was the last post?

When googling firefox sync "x-keyid", there was essentially 3 results.

  1. The very page I came from.
  2. A Bugzilla issue by our friend Ryan for the TokenServer to accept OAuth tokens.
  3. The sync-dev@mozilla.org mailing list on mail-archive.com, not a specific thread but it turned out that the latest messages on the list before it gets migrated to Google Groups earlier this year were about the X-KeyID header.

The second link doesnā€™t say anything about X-KeyID, and while it links a few other resources, they donā€™t mention this header either (but they turned out to be critical when I later tried to implement the full OAuth flow).

Finally, in the mailing list thread, the OP seems to be trying to do exactly the same thing as me, and they apparently got a step further because they have a whole algorithm to compute the X-KeyID header, involving a number of parameters I donā€™t have and some key derivation logic. Iā€™m not sure where that algorithm comes from, but whatā€™s clear from the mail is that itā€™s not working.

Ryan delivers one more time by replying with an explanation and a link to the production code generating the said key.

For legacy backwards compatibility reasons, the key-derivation for Sync is different than the derivation for general FxA scoped keys. The simplest way to explain the differences is probably to link to the code we have here, which does the derivation.

This is both a good and a bad news for me.

The good news is that with the algorithm from the original message of the thread, plus the code in Ryanā€™s link, I should be able to generate a working X-KeyID which might be all I need to make my OAuth version work!

The bad news is that this key deriving code Iā€™m going to rely on is called _deriveLegacySyncKey, and you know form the beginning of this very post that I donā€™t like using code thatā€™s called ā€œlegacyā€, because it most necessarily means that thereā€™s a better alternative.

But letā€™s put that aside for now. This will be an adventure for another day. For now weā€™re so close to getting this code work that I canā€™t just move on to something else right now.

Tracking keyRotationTimestamp

I start with the Python code form the original email:

kid = str(keyRotationTimestamp) + '-' + base64.urlsafe_b64encode(tmp[:16]).decode('utf-8').rstrip('=')

The first thing we see is that we need keyRotationTimestamp, and I happen to not have encountered anything named keyRotationTimestamp yet.

I look it up on the API documentation of Firefox Accounts which Iā€™m already on, hoping that itā€™s returned by some endpoint there but no luck.

Fuck

What follows is a number of searches:

Followed by me searching that string directly on all of GitHub and browsing the first couple pages of results (out of 49) without luck.

Then I tried to scope my search to the mozilla/fxa repo which had been a good source of information in the past, and bingo! The first result is from the /account/scoped-key-data endpoint of fxa-auth-server (you know, the Firefox Accounts server), which do return the keyRotationTimestamp! Itā€™s just that the documentation I checked earlier doesnā€™t include the details of the payload for this endpoint.

This one too, has a neat matching function in fxa-js-client and I can move to the next step.

Actually computing the X-KeyID header

Letā€™s get back one more time to the Python code from the email weā€™re trying to adapt.

kid = str(keyRotationTimestamp) + '-' + base64.urlsafe_b64encode(tmp[:16]).decode('utf-8').rstrip('=')

Now we figured the first part, the rest is the Base64URL representation of the first 16 bytes of tmp, which is the result of some key derivation. According to the email thread, the key derivation part isnā€™t working, so weā€™re not going to try to port it, but Base64URL encoding the first 16 bytes of a key reminds me of something.

We can also see this pattern in the code Ryan pointed to in order to address the key derivation issue:

scopedKey.kid = options.keyRotationTimestamp + '-' + base64url(kHash.slice(0, 16))

While itā€™s not immediately clear to me what kHash is, the base64url(kHash.slice(0, 16)) is again awfully familiar. It is exactly what we used to do to compute the X-Client-State header for the BrowserID version!

const clientState = sha256(syncKey).slice(0, 16).toString('hex')

This is especially promising since the Python code behind the TokenServer also calls it client_state:

kid = request.headers.get('X-KeyID')
keys_changed_at, client_state = parse_key_id(kid)

So I try to combine the keyRotationTimestamp I just retrieved with the hexadecimal representation of my previous X-Client-State, and guess what. It works!

I want to see the code!

The great news is that this not only makes our code use the latest and greatest way to connect to Firefox Sync, without relying on anything legacy or deprecated, but it also makes our implementation much simpler!

Hereā€™s the updated version of the code we previously built in this series, up to getting the Sync token from the TokenServer. Calling the Sync API from there is not affected, so I wonā€™t include it again here.

const crypto = require('crypto')
const fetch = require('node-fetch')
const AuthClient = require('fxa-js-client')

const authServerUrl = 'https://api.accounts.firefox.com/v1'
const tokenServerUrl = 'https://token.services.mozilla.com'
const scope = 'https://identity.mozilla.com/apps/oldsync'
const clientId = '...'
const email = '...'
const pass = '...'

async function main () {
  const client = new AuthClient(authServerUrl)

  const creds = await client.signIn(email, pass, {
    keys: true,
    reason: 'login'
  })

  const accountKeys = await client.accountKeys(creds.keyFetchToken, creds.unwrapBKey)
  const oauthToken = await client.createOAuthToken(creds.sessionToken, clientId, { scope })
  const scopedKeyData = await client.getOAuthScopedKeyData(creds.sessionToken, clientId, scope)

  const syncKey = Buffer.from(accountKeys.kB, 'hex')
  const clientState = crypto.createHash('sha256').update(syncKey).digest().slice(0, 16).toString('base64url')
  const keyId = `${scopedKeyData[scope].keyRotationTimestamp}-${clientState}`

  // See <https://github.com/mozilla-services/tokenserver#using-oauth>.
  const token = await fetch(`${tokenServerUrl}/1.0/sync/1.5`, {
    headers: {
      Authorization: `Bearer ${oauthToken.access_token}`,
      'X-KeyID': keyId
    }
  })
    .then(res => res.json())
}

main()

Hereā€™s the equivalent code from the previous article for comparison:

Legacy BrowserID code
const { promisify } = require('util')
const crypto = require('crypto')
const fetch = require('node-fetch')
const AuthClient = require('fxa-js-client')
const njwt = require('njwt')

const authServerUrl = 'https://api.accounts.firefox.com/v1'
const tokenServerUrl = 'https://token.services.mozilla.com'
const email = '...'
const pass = '...'

function base64to10 (data) {
  return BigInt('0x' + Buffer.from(data, 'base64').toString('hex')).toString(10)
}

async function main () {
  const client = new AuthClient(authServerUrl)

  const creds = await client.signIn(email, pass, {
    keys: true,
    reason: 'login'
  })

  const accountKeys = await client.accountKeys(creds.keyFetchToken, creds.unwrapBKey)

  const kp = await promisify(crypto.generateKeyPair)('rsa', {
    modulusLength: 2048
  })

  const jwk = kp.publicKey.export({ format: 'jwk' })

  const publicKey = {
    algorithm: jwk.algorithm.slice(0, 2),
    n: base64to10(jwk.n),
    e: base64to10(jwk.e)
  }

  // Time interval in milliseconds until the certificate will expire, up to a
  // maximum of 24 hours as documented in <https://github.com/mozilla/fxa/blob/f6bc0268a9be12407456fa42494243f336d81a38/packages/fxa-auth-server/docs/api.md#request-body-32>.
  const duration = 1000 * 60 * 60 * 24

  const { cert } = await client.certificateSign(creds.sessionToken, publicKey, duration)

  // Generate an "identity assertion" which is a JWT as documented in
  // <https://github.com/mozilla/id-specs/blob/prod/browserid/index.md#identity-assertion>.
  const signedObject = njwt.create({ aud: tokenServerUrl, iss: authServerUrl }, kp.privateKey, 'RS256')
    .setClaim('exp', Date.now() + duration)
    .compact()

  // Certs are separated by a `~` as documented in <https://github.com/mozilla/id-specs/blob/prod/browserid/index.md#backed-identity-assertion>.
  const backedAssertion = [cert, signedObject].join('~')

  // See <https://github.com/mozilla-services/tokenserver#using-browserid>.
  const syncKey = Buffer.from(accountKeys.kB, 'hex')
  const clientState = crypto.createHash('sha256').update(syncKey).digest().slice(0, 16).toString('hex')

  const token = await fetch(`${tokenServerUrl}/1.0/sync/1.5`, {
    headers: {
      Authorization: `BrowserID ${backedAssertion}`,
      'X-Client-State': clientState
    }
  })
    .then(res => res.json())
}

main()

But while this is a solid improvement from what we had previously built since the beginning of this series, thereā€™s still a bit more to unwrap.

Going further

You can probably tell by now that I love digging into rabbit holes and Iā€™m eternally unsatisfied.

While I thought that I had found the last piece of the puzzle with the OAuth method described in this post, in order to integrate with Firefox Sync as cleanly as possible, it occurred to me that something was off as I was trying to explain it.

One of the main benefits of OAuth is to be able to grant granular permissions to a third-party service, without giving them knowledge of your password.

Yet with the solution from this post, not only do we still need to have knowledge of the userā€™s password in order to login with the email/password scheme and derive the Sync encryption keys, but this method also grants us a Firefox Accounts session token which allows us to do virtually anything to that user account through the API. This defeats both advantages of OAuth mentioned above; permissions are not granular and we have access to the plaintext password.

God mode quote context

This authentication scheme is even referred by Ryan as ā€œgod modeā€ in a comment on the OAuth flow spec on Google Docs (that I encountered earlier through a Bugzilla issue).

Imagine a third-party browser that does its own Sync implementation, being able to authenticate to our Sync service using standard OAuth-style flow rather than the ā€œgod modeā€ integration that [they] currently do, where they basically prompt for full access to your account.

This is a concern thatā€™s also addressed in the introduction of the document:

Key material can only be accessed through a bespoke authorization protocol that is [ā€¦] far too powerful. The protocol gives the application complete control of the userā€™s Firefox Account, and hands it a copy of their master key material. There is currently no provision for scoping access down to a subset of data or capabilities.

In the final article (for real this time, I promise) weā€™ll see how to use the full OAuth flow to authenticate to Firefox Accounts and access Firefox Sync, so that we never have knowledge of the userā€™s password, and request only the permissions that we need instead of full access.

Check out the other posts in this series!

  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

Want to leave a comment?

Join the discussion on Twitter or send me an email! šŸ’Œ
This post helped you? Buy me a coffee! šŸ»