Created
October 11, 2019 09:33
-
-
Save Jimmy-Z/184acceecbd5f435281d86f25ce97dec to your computer and use it in GitHub Desktop.
Blizzard Authenticator to TOTP convertor
This file contains 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
#!/usr/bin/env python3 | |
# distilled from https://github.com/jleclanche/python-bna | |
# only remain functionality is blizzard authenticator serial + restore code => TOTP secret conversion | |
# so you can use 3rd party TOTP applications | |
# contained in a single file so it could be audited easily and you don't have to use pip | |
# and support "CN-" serial courtesy of https://github.com/winauth/winauth/ | |
import hmac | |
from base64 import b32encode | |
from hashlib import sha1 | |
from http.client import HTTPConnection | |
from secrets import token_bytes | |
from typing import Optional | |
import sys | |
# constants.py | |
RSA_MOD = 104890018807986556874007710914205443157030159668034197186125678960287470894290830530618284943118405110896322835449099433232093151168250152146023319326491587651685252774820340995950744075665455681760652136576493028733914892166700899109836291180881063097461175643998356321993663868233366705340758102567742483097 # noqa | |
RSA_KEY = 257 | |
HOST = "mobile-service.blizzard.com" | |
HOST_CN = "mobile-service.battlenet.com.cn" | |
PATH_INIT_RESTORE = "/enrollment/initiatePaperRestore.htm" | |
PATH_VALIDATE_RESTORE = "/enrollment/validatePaperRestore.htm" | |
# utils.py | |
def normalize_serial(serial: str) -> str: | |
return serial.upper().replace("-", "").strip() | |
def prettify_serial(serial: str) -> str: | |
""" | |
Returns the prettified version of a serial | |
It should look like XX-AAAA-BBBB-CCCC-DDDD | |
""" | |
serial = normalize_serial(serial) | |
if len(serial) != 14: | |
raise ValueError("serial %r should be 14 characters long" % (serial)) | |
def digits(chars): | |
if not chars.isdigit(): | |
raise ValueError("bad serial %r" % (serial)) | |
return "%04i" % int((chars)) | |
return "%s-%s-%s-%s" % ( | |
serial[0:2].upper(), | |
digits(serial[2:6]), | |
digits(serial[6:10]), | |
digits(serial[10:14]), | |
) | |
# crypto.py | |
def encrypt(data: bytes) -> str: | |
base_num = int(data.hex(), 16) | |
n = base_num ** RSA_KEY % RSA_MOD | |
ret = "" | |
while n > 0: | |
n, m = divmod(n, 256) | |
ret = chr(m) + ret | |
return ret | |
def decrypt(response: bytes, otp: bytes) -> bytearray: | |
ret = bytearray() | |
for c, e in zip(response, otp): | |
ret.append(c ^ e) | |
return ret | |
def restore_code_to_bytes(code: str) -> bytes: | |
ret = bytearray() | |
for c in code: | |
i = ord(c) | |
if 58 > i > 47: | |
i -= 48 | |
else: | |
mod = i - 55 | |
if i > 72: | |
mod -= 1 | |
if i > 75: | |
mod -= 1 | |
if i > 78: | |
mod -= 1 | |
if i > 82: | |
mod -= 1 | |
i = mod | |
ret.append(i) | |
return bytes(ret) | |
# http.py | |
class HTTPError(Exception): | |
def __init__(self, msg, response): | |
self.response = response | |
super().__init__(msg) | |
def get_server_response(data: Optional[str], host: str, path: str) -> bytes: | |
conn = HTTPConnection(host) | |
conn.request("POST", path, data) | |
response = conn.getresponse() | |
if response.status != 200: | |
raise HTTPError("%s returned status %i" % (host, response.status), response) | |
ret = response.read() | |
conn.close() | |
return ret | |
def restore(serial: str, restore_code: str) -> str: | |
restore_code = restore_code.upper() | |
serial = normalize_serial(serial) | |
if len(restore_code) != 10: | |
raise ValueError(f"invalid restore code (should be 10 characters): {restore_code}") | |
if serial[0:2] == "CN": | |
host = HOST_CN | |
else: | |
host = HOST | |
challenge = initiate_paper_restore(host, serial) | |
if len(challenge) != 32: | |
raise ValueError("Bad challenge length (expected 32, got %i)" % (len(challenge))) | |
code = restore_code_to_bytes(restore_code) | |
hash = hmac.new(code, serial.encode() + challenge, digestmod=sha1).digest() | |
otp = token_bytes(20) | |
e = encrypt(hash + otp) | |
response = validate_paper_restore(host, serial + e) | |
secret = decrypt(response, otp) | |
return b32encode(secret).decode() | |
def initiate_paper_restore(host:str, serial: str) -> bytes: | |
return get_server_response(serial, host, PATH_INIT_RESTORE) | |
def validate_paper_restore(host:str, data: str) -> bytes: | |
try: | |
response = get_server_response(data, host, PATH_VALIDATE_RESTORE) | |
except HTTPError as e: | |
if e.response.status == 600: | |
raise HTTPError("Invalid serial or restore key", e.response) | |
else: | |
raise | |
return response | |
# main | |
if __name__ == "__main__": | |
print("serial: ", file = sys.stderr, end = "") | |
serial = input() | |
print("restore code: ", file = sys.stderr, end = "") | |
restore_code = input() | |
secret = restore(serial, restore_code) | |
print("secret: {}".format(secret), file = sys.stderr) | |
serial = prettify_serial(serial) | |
uri = "otpauth://totp/{issuer}:{serial}?secret={secret}&issuer={issuer}&digits=8".format( | |
issuer = "Blizzard", serial = serial, secret = secret) | |
# pipe to qrencode like `./ba2totp.py |qrencode -t utf8` | |
print(uri, end = "") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment