def test_password_stretching_and_key_derivation(self): # These are the test vectors from the onepw protocol document. email = "andr\[email protected]" self.assertEqual( email.encode("utf8"), dehexlify(""" 616e6472c3a94065 78616d706c652e6f 7267 """)) pwd = "p\xe4ssw\xf6rd" self.assertEqual( pwd.encode("utf8"), dehexlify(""" 70c3a4737377c3b6 7264 """)) qspwd = quick_stretch_password(email, pwd) self.assertEqual( qspwd, dehexlify(""" e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d """)) authpw = derive_key(qspwd, "authPW") self.assertEqual( authpw, dehexlify(""" 247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375 """)) ubkey = derive_key(qspwd, "unwrapBkey") self.assertEqual( ubkey, dehexlify(""" de6a2648b78284fc b9ffa81ba9580330 9cfba7af583c01a8 a1a63e567234dd28 """))
def login(user): client = Client("https://api.accounts.firefox.com") session = client.login(user['email'], user['password'], keys=True) keyA,keyB = session.fetch_keys() info = b"identity.mozilla.com/picl/v1/oldsync" namespace = b"oldsync" keys = derive_key(secret=keyB, namespace=namespace, size=64) encryption_key = keys[0:32] hmac_key = keys[32:64] # TODO: Store this or a derived longer-lived token # Causes a login event which causes an email # TODO: Should move to use OAuth which solves the long-term cred storage # issue fxab = FxABrowserIDAuth(user['email'], user['password'], with_client_state=True) raw_resp = requests.get('https://token.services.mozilla.com/1.0/sync/1.5', auth=fxab) raw_resp.raise_for_status() hawk_resp = raw_resp.json() return { "hawk_resp": hawk_resp, "hawk_uid": hawk_resp['uid'], "hawk_hashalg": hawk_resp['hashalg'], "hawk_api_endpoint": hawk_resp['api_endpoint'], "hawk_duration": hawk_resp['duration'], "hawk_key": hawk_resp['key'], "hawk_hashed_fxa_uid": hawk_resp['hashed_fxa_uid'], "hawk_id": hawk_resp['id'], 'encryption_key': encryption_key.hex(), 'hmac_key': hmac_key.hex(), }
def login(self, email, password=None, stretchpwd=None, keys=False, unblock_code=None): stretchpwd = self._get_stretched_password(email, password, stretchpwd) body = { "email": email, "authPW": hexstr(derive_key(stretchpwd, "authPW")), } url = "/account/login" if keys: url += "?keys=true" if unblock_code: body["unblockCode"] = unblock_code resp = self.apiclient.post(url, body) # XXX TODO: somehow sanity-check the schema on this endpoint return Session( client=self, email=email, stretchpwd=stretchpwd, uid=resp["uid"], token=resp["sessionToken"], key_fetch_token=resp.get("keyFetchToken"), verified=resp["verified"], verificationMethod=resp.get("verificationMethod"), auth_timestamp=resp["authAt"], )
def create_account(self, email, password=None, stretchpwd=None, **kwds): keys = kwds.pop("keys", False) stretchpwd = self._get_stretched_password(email, password, stretchpwd) body = { "email": email, "authPW": hexstr(derive_key(stretchpwd, "authPW")), } EXTRA_KEYS = ("service", "redirectTo", "resume", "preVerifyToken", "preVerified") for extra in kwds: if extra in EXTRA_KEYS: body[extra] = kwds[extra] else: msg = "Unexpected keyword argument: {0}".format(extra) raise TypeError(msg) url = "/account/create" if keys: url += "?keys=true" resp = self.apiclient.post(url, body) # XXX TODO: somehow sanity-check the schema on this endpoint return Session( client=self, email=email, stretchpwd=stretchpwd, uid=resp["uid"], token=resp["sessionToken"], key_fetch_token=resp.get("keyFetchToken"), verified=False, auth_timestamp=resp["authAt"], )
def finish_password_change(self, token, stretchpwd, wrapkb): body = { "authPW": hexstr(derive_key(stretchpwd, "authPW")), "wrapKb": hexstr(wrapkb), } auth = HawkTokenAuth(token, "passwordChangeToken", self.apiclient) self.apiclient.post("/password/change/finish", body, auth=auth)
def fetch_keys(self, key_fetch_token, stretchpwd): url = "/account/keys" auth = HawkTokenAuth(key_fetch_token, "keyFetchToken", self.apiclient) resp = self.apiclient.get(url, auth=auth) bundle = unhexlify(resp["bundle"]) keys = auth.unbundle("account/keys", bundle) unwrap_key = derive_key(stretchpwd, "unwrapBkey") return (keys[:32], xor(keys[32:], unwrap_key))
def reset_account(self, email, token, password=None, stretchpwd=None): stretchpwd = self._get_stretched_password(email, password, stretchpwd) body = { "authPW": hexstr(derive_key(stretchpwd, "authPW")), } url = "/account/reset" auth = HawkTokenAuth(token, "accountResetToken", self.apiclient) self.apiclient.post(url, body, auth=auth)
def destroy_account(self, email, password=None, stretchpwd=None): stretchpwd = self._get_stretched_password(email, password, stretchpwd) body = { "email": email, "authPW": hexstr(derive_key(stretchpwd, "authPW")), } url = "/account/destroy" self.apiclient.post(url, body)
def change_password(self, oldpwd, newpwd): stretched_oldpwd = quick_stretch_password(self.email, oldpwd) resp = self.start_password_change(stretched_oldpwd) keys = self.fetch_keys(resp["keyFetchToken"], stretched_oldpwd) token = resp["passwordChangeToken"] stretched_newpwd = quick_stretch_password(self.email, newpwd) new_wrapkb = xor(keys[1], derive_key(stretched_newpwd, "unwrapBkey")) self.finish_password_change(token, stretched_newpwd, new_wrapkb)
def change_password(self, email, oldpwd=None, newpwd=None, oldstretchpwd=None, newstretchpwd=None): oldstretchpwd = self._get_stretched_password(email, oldpwd, oldstretchpwd) newstretchpwd = self._get_stretched_password(email, newpwd, newstretchpwd) resp = self.start_password_change(email, oldstretchpwd) keys = self.fetch_keys(resp["keyFetchToken"], oldstretchpwd) token = resp["passwordChangeToken"] new_wrapkb = xor(keys[1], derive_key(newstretchpwd, "unwrapBkey")) self.finish_password_change(token, newstretchpwd, new_wrapkb)
def test_password_stretching_and_key_derivation(self): # These are the test vectors from the onepw protocol document. email = u"andr\[email protected]" self.assertEqual(email.encode("utf8"), dehexlify(""" 616e6472c3a94065 78616d706c652e6f 7267 """)) pwd = u"p\xe4ssw\xf6rd" self.assertEqual(pwd.encode("utf8"), dehexlify(""" 70c3a4737377c3b6 7264 """)) qspwd = quick_stretch_password(email, pwd) self.assertEqual(qspwd, dehexlify(""" e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d """)) authpw = derive_key(qspwd, "authPW") self.assertEqual(authpw, dehexlify(""" 247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375 """)) ubkey = derive_key(qspwd, "unwrapBkey") self.assertEqual(ubkey, dehexlify(""" de6a2648b78284fc b9ffa81ba9580330 9cfba7af583c01a8 a1a63e567234dd28 """))
def get_encryption_key(client, login, password): keyB = __get_sync_key(login, password) # get_encryption key crypto_key = client.get_record(collection = 'crypto', record_id = 'keys') sync_key_bundle = derive_key(keyB, b'oldsync', 32 * 2) sync_key_bundle = KeyBundle(key = sync_key_bundle[:32], hmac_key = sync_key_bundle[32:]) data = decrypt_data(sync_key_bundle, crypto_key['payload']) encryption_key_bundle = KeyBundle(key = b64decode(data['default'][0]), hmac_key = b64decode(data['default'][1])) return encryption_key_bundle
def login(user): """ Logs a user into their Firefox account and returns tempoary credentials for use by AuthRequest. """ # TODO: pull out the urls to be part of the config. client = Client("https://api.accounts.firefox.com") session = client.login(user['email'], user['password'], keys=True) keyA, keyB = session.fetch_keys() # Magic strings from the docs # https://moz-services-docs.readthedocs.io/en/latest/sync/storageformat5.html info = b"identity.mozilla.com/picl/v1/oldsync" namespace = b"oldsync" keys = derive_key(secret=keyB, namespace=namespace, size=64) encryption_key = keys[0:32] hmac_key = keys[32:64] # TODO: Store this or a derived longer-lived token # Causes a login event which causes an email # TODO: Should move to use OAuth which solves the long-term cred storage # issue fxab = FxABrowserIDAuth(user['email'], user['password'], with_client_state=True) raw_resp = requests.get('https://token.services.mozilla.com/1.0/sync/1.5', auth=fxab) raw_resp.raise_for_status() hawk_resp = raw_resp.json() return { "hawk_resp": hawk_resp, "hawk_uid": hawk_resp['uid'], "hawk_hashalg": hawk_resp['hashalg'], "hawk_api_endpoint": hawk_resp['api_endpoint'], "hawk_duration": hawk_resp['duration'], "hawk_key": hawk_resp['key'], "hawk_hashed_fxa_uid": hawk_resp['hashed_fxa_uid'], "hawk_id": hawk_resp['id'], 'encryption_key': encryption_key.hex(), 'hmac_key': hmac_key.hex(), }
def fetch_keys(self, key_fetch_token=None, stretchpwd=None): # Use values from session construction, if not overridden. if key_fetch_token is None: key_fetch_token = self._key_fetch_token if key_fetch_token is None: # XXX TODO: what error? raise RuntimeError("missing key_fetch_token") if stretchpwd is None: stretchpwd = self._stretchpwd if stretchpwd is None: # XXX TODO: what error? raise RuntimeError("missing stretchpwd") # Fetch the keys, and clear cached values from session construction. url = "/v1/account/keys" auth = HawkTokenAuth(key_fetch_token, "keyFetchToken", self.apiclient) resp = self.apiclient.get(url, auth=auth) self._key_fetch_token = None self._stretchpwd = None # Decrypt kB using the stretchpwd. bundle = unhexlify(resp["bundle"]) keys = auth.unbundle("account/keys", bundle) unwrap_key = derive_key(stretchpwd, "unwrapBkey") self.keys = (keys[:32], xor(keys[32:], unwrap_key)) return self.keys
hawk_resp = None encryption_key = None hmac_key = None if 'hawk' not in config or 'fxa' not in config: user = config['user'] client = Client("https://api.accounts.firefox.com") session = client.login(user['email'], user['password'], keys=True) keyA, keyB = session.fetch_keys() info = b"identity.mozilla.com/picl/v1/oldsync" namespace = b"oldsync" keys = derive_key(secret=keyB, namespace=namespace, size=64) encryption_key = keys[0:32] hmac_key = keys[32:64] # TODO: Store this or a derived longer-lived token # Causes a login event which causes an email fxab = FxABrowserIDAuth(user['email'], user['password'], with_client_state=True) raw_resp = requests.get('https://token.services.mozilla.com/1.0/sync/1.5', auth=fxab) raw_resp.raise_for_status() hawk_resp = raw_resp.json() config['hawk'] = hawk_resp config['fxa'] = {
def start_password_change(self, email, stretchpwd): body = { "email": email, "oldAuthPW": hexstr(derive_key(stretchpwd, "authPW")), } return self.apiclient.post("/password/change/start", body)