Skip to content

Instantly share code, notes, and snippets.

@rfk
Last active July 10, 2018 22:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rfk/0462fc6c81e43f2b9abbea41c479a784 to your computer and use it in GitHub Desktop.
Save rfk/0462fc6c81e43f2b9abbea41c479a784 to your computer and use it in GitHub Desktop.
Test Code for FxA Account Recovery Keys
#
# To run this script, you'll need to install some dependencies:
#
# $> pip instal hkdf jwcrypto crockford
#
# Then you can run it without arguments, and it will print out
# a series of test vectors for checking compatibility of other
# implementations of Recovery Keys.
#
# $> python ./recovery_key_test_vectors.py
#
import json
import hashlib
from binascii import hexlify, unhexlify
from base64 import urlsafe_b64encode
import hkdf
import crockford
import jwcrypto.jwe, jwcrypto.jwk, jwcrypto.jwa
# For compatibility with JS, a compact JSON stringify.
def json_stringify(obj):
return json.dumps(obj, separators=(',', ':'))
# From the recovery process, we learn the account uid.
uid = "aaaaabbbbbcccccdddddeeeeefffff00"
assert len(uid) == 32
print "uid =", uid
# The user provides the recovery key in crockford-base32-encoded form.
recovery_key = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
assert len(recovery_key) == 32
print "recovery_key =", recovery_key
# XXX TODO: It should have a leading "1" as version identifier.
#assert recovery_key[0] == "1"
recovery_key_raw = crockford.b32decode(recovery_key)
# From the recovery key, we derive a key id and an encryption key.
deriver = hkdf.Hkdf(unhexlify(uid), recovery_key_raw, hash=hashlib.sha256)
recover_kid = hexlify(deriver.expand(b"fxa recovery fingerprint", 16))
assert len(recover_kid) == 32
print "recover-kid = ", recover_kid
recover_enc = hexlify(deriver.expand(b"fxa recovery encrypt key", 32))
assert len(recover_enc) == 64
print "recover-enc = ", recover_enc
# We're going to be encrypting a JSON payload containing kB.
kB = "000000111111222222333333444444555555666666777777888888999999ABCD"
assert len(kB) == 64
print "kB =", kB
recover_data_plaintext = json_stringify({ "kB": kB })
print "recover-data plaintext =", recover_data_plaintext
# For testing purposes we need to generate a JWE using a fixed IV,
# which for security reasons is not supported through the public API.
# This is a bit of light hackery to achieve that:
iv = 'eeddccbbaa99887766554433'
assert len(iv) == 24
print "IV =", iv
jwcrypto.jwa._randombits = lambda s: unhexlify(iv)
# We bundle the plaintext into a JWE in compact representation.
# To ensure compatibility with javascript implementation, we
# construct the header field directly as a string.
header = '{"enc":"A256GCM","alg":"dir","kid":"' + recover_kid + '"}'
jwe = jwcrypto.jwe.JWE(recover_data_plaintext, header)
jwe.add_recipient(jwcrypto.jwk.JWK(kty="oct", k=urlsafe_b64encode(unhexlify(recover_enc))))
recover_data = jwe.serialize(compact=True)
print "recover-data =", recover_data
# For completeness, let's check that we can recover that data!
jwe = jwcrypto.jwe.JWE()
jwe.deserialize(recover_data)
jwe.decrypt(jwcrypto.jwk.JWK(kty="oct", k=urlsafe_b64encode(unhexlify(recover_enc))))
print "recover-data decrypts to:", jwe.payload
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment