Last active
July 10, 2018 22:45
-
-
Save rfk/0462fc6c81e43f2b9abbea41c479a784 to your computer and use it in GitHub Desktop.
Test Code for FxA Account Recovery Keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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