Skip to content

Instantly share code, notes, and snippets.

@Jimmy-Z
Created October 11, 2019 09:33
Show Gist options
  • Save Jimmy-Z/184acceecbd5f435281d86f25ce97dec to your computer and use it in GitHub Desktop.
Save Jimmy-Z/184acceecbd5f435281d86f25ce97dec to your computer and use it in GitHub Desktop.
Blizzard Authenticator to TOTP convertor
#!/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