Permalink
Browse files

First public release

  • Loading branch information...
1 parent 6a32a0c commit b3be10f10cbd9ba349ed08fd5529968ea0979687 @khertan committed Jun 11, 2012
View
@@ -1,4 +1,4 @@
KhtBMA
======
-A battle.net authenticator for n9 and n950 Meego Device
+An unofficial battle.net authenticator for n9 and n950 Meego Devices
View
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=KhtBMA
+Exec=/usr/bin/invoker --type=e -n -s python /opt/khtbma/__init__.py
+Icon=khtbma
+
View
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=net.khertan.khtbma
+Exec=/opt/khtbma/__init__.py
+
View
@@ -0,0 +1,138 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011 Benoit HERVIER <khertan@khertan.net>
+# Licenced under GPLv3
+
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+
+from PySide.QtGui import QApplication
+from PySide.QtCore import QUrl, Slot, QObject, Property, Signal, QAbstractListModel, QModelIndex, QSettings
+from PySide import QtDeclarative
+from PySide.QtOpenGL import QGLWidget
+from binascii import hexlify, unhexlify
+
+import sys
+import os
+import os.path
+import datetime
+
+import bna
+
+__author__ = 'Benoit HERVIER (Khertan)'
+__email__ = 'khertan@khertan.net'
+__version__ = '1.0.0'
+
+class Authenticator(QObject):
+
+ ''' A class for manipulating tocken'''
+ def __init__(self, ):
+ QObject.__init__(self)
+ self._settings = QSettings()
+ self._token = ''
+ self._timeremaining = ''
+ self._serial = ''
+
+ if self._settings.contains('REGION'):
+ self._region = self._settings.value('REGION')
+ if self._region not in ('EU', 'US'):
+ self._set_region('US')
+ else:
+ self._region = 'US'
+
+ if self._settings.contains('SECRET'):
+ self._secret = unhexlify(self._settings.value('SECRET'))
+ if not self._secret:
+ self.new_serial()
+ else:
+ self.new_serial()
+
+ if not self._serial:
+ if self._settings.contains('SERIAL'):
+ self._serial = self._settings.value('SERIAL')
+
+ if self._token == '':
+ self.sync()
+
+ @Slot()
+ def new_serial(self):
+ try:
+ authenticator = bna.requestNewSerial(self._region)
+ self._secret = authenticator["secret"]
+ self._set_serial(authenticator["serial"])
+ self._settings.setValue("SECRET", hexlify(self._secret).decode("ascii"))
+ self._settings.setValue("SERIAL", self._serial)
+ self.sync()
+ except bna.HTTPError, e:
+ self.on_error.emit("Could not connect: %s" % (unicode(e)))
+
+ @Slot()
+ def sync(self):
+ try:
+ token, self._timeremaining = bna.getToken(secret=self._secret)
+ self._token = unicode(token)
+ self.on_tokenChanged.emit()
+ self.on_timeremainingChanged.emit()
+ except Exception, e:
+ self.on_error.emit("Could not connect: %s" % (unicode(e)))
+
+ def _get_region(self):
+ return self._region
+ def _set_region(self, region):
+ self._region = region
+ self._settings.setValue("REGION", self._region)
+ self.on_regionChanged.emit()
+ def _get_serial(self):
+ return self._serial
+ def _set_serial(self, serial):
+ self._serial = serial
+ self._settings.setValue("SERIAL", self._serial)
+ self.on_serialChanged.emit()
+
+ def _get_token(self):
+ return self._token
+ def _get_timeremaining(self):
+ return self._timeremaining
+
+ on_tokenChanged = Signal()
+ on_timeremainingChanged = Signal()
+
+ on_serialChanged = Signal()
+ on_regionChanged = Signal()
+ serial = Property(unicode, _get_serial, _set_serial, notify=on_serialChanged)
+ region = Property(unicode, _get_region, _set_region, notify=on_regionChanged)
+ token = Property(unicode, _get_token, notify=on_tokenChanged)
+ timeremaining = Property(int, _get_timeremaining, notify=on_timeremainingChanged)
+ on_error = Signal(unicode)
+
+class KhtBMA(QApplication):
+ ''' Application class '''
+ def __init__(self):
+ QApplication.__init__(self, sys.argv)
+ self.setOrganizationName("Khertan Software")
+ self.setOrganizationDomain("khertan.net")
+ self.setApplicationName("KhtBMA")
+
+ self.view = QtDeclarative.QDeclarativeView()
+ self.glw = QGLWidget()
+ self.view.setViewport(self.glw)
+ self.auth = Authenticator()
+ self.rootContext = self.view.rootContext()
+ self.rootContext.setContextProperty("argv", sys.argv)
+ self.rootContext.setContextProperty("__version__", __version__)
+ self.rootContext.setContextProperty('Authenticator',self.auth)
+ self.view.setSource(QUrl.fromLocalFile(
+ os.path.join(os.path.dirname(__file__), 'qml', 'main.qml')))
+ self.rootObject = self.view.rootObject()
+ self.auth.on_error.connect(self.rootObject.onError)
+ self.view.showFullScreen()
+
+if __name__ == '__main__':
+ sys.exit(KhtBMA().exec_())
View
@@ -0,0 +1,233 @@
+"""
+python-bna
+Battle.net Authenticator routines in Python.
+
+Specification can be found here:
+ <http://bnetauth.freeportal.us/specification.html>
+Python implementation by Jerome Leclanche <jerome.leclanche@gmail.com>
+"""
+
+__version__ = "3.0"
+
+import hmac
+from binascii import hexlify
+from hashlib import sha1
+try:
+ from http.client import HTTPConnection
+except ImportError:
+ from httplib import HTTPConnection
+from time import time
+
+RSA_MOD = 104890018807986556874007710914205443157030159668034197186125678960287470894290830530618284943118405110896322835449099433232093151168250152146023319326491587651685252774820340995950744075665455681760652136576493028733914892166700899109836291180881063097461175643998356321993663868233366705340758102567742483097
+RSA_KEY = 257
+
+ENROLL_HOSTS = {
+ "EU": "m.eu.mobileservice.blizzard.com",
+ "US": "m.us.mobileservice.blizzard.com",
+ #"EU": "eu.mobile-service.blizzard.com",
+ #"US": "us.mobile-service.blizzard.com",
+ "default": "mobile-service.blizzard.com",
+}
+
+class HTTPError(Exception):
+ def __init__(self, msg, response):
+ self.response = response
+ super(HTTPError, self).__init__(msg)
+
+def getOneTimePad(length):
+ def timedigest():
+ return sha1(str(time()).encode()).digest()
+
+ return (timedigest() + timedigest())[:length]
+
+def getServerResponse(data, host, path):
+ """
+ Send computed data to Blizzard servers
+ Return the answer from the server
+ """
+ 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 enroll(data, host=ENROLL_HOSTS["default"], path="/enrollment/enroll.htm"):
+ return getServerResponse(data, host, path)
+
+def encrypt(data):
+ data = int(hexlify(data), 16)
+ n = data ** RSA_KEY % RSA_MOD
+ ret = ""
+ while n > 0:
+ n, m = divmod(n, 256)
+ ret = chr(m) + ret
+ return ret
+
+def decrypt(response, otp):
+ ret = bytearray()
+ for c, e in zip(response, otp):
+ # python2 compatibility
+ if isinstance(c, str):
+ c = ord(c)
+ e = ord(e)
+
+ ret.append(c ^ e)
+ return ret
+
+def requestNewSerial(region="US", model="Motorola RAZR v3"):
+ """
+ Requests a new authenticator
+ This will connect to the Blizzard servers
+ """
+ def baseMsg(otp, region, model):
+ ret = (otp + b"\0" * 37)[:37]
+ ret += region.encode() or b"\0\0"
+ ret += (model.encode() + b"\0" * 16)[:16]
+ return b"\1" + ret
+
+ otp = getOneTimePad(37)
+ data = baseMsg(otp, region, model)
+
+ e = encrypt(data)
+ host = ENROLL_HOSTS.get(region, ENROLL_HOSTS["default"]) # get the host, or fallback to default
+ response = decrypt(enroll(e, host)[8:], otp)
+
+ secret = bytes(response[:20])
+ serial = response[20:].decode()
+
+ region = serial[:2]
+ if region not in ("EU", "US"):
+ raise ValueError("Unexpected region: %r" % (region))
+
+ return {"serial": serial, "secret": secret}
+
+def bytesToRestoreCode(digest):
+ ret = []
+ for i in digest:
+ c = i & 0x1f
+ if c < 10:
+ c += 48
+ else:
+ c += 55
+ if c > 72: # I
+ c += 1
+ if c > 75: # L
+ c += 1
+ if c > 78: # O
+ c += 1
+ if c > 82: # S
+ c += 1
+ ret.append(chr(c))
+
+ return "".join(ret)
+
+def getRestoreCode(serial, secret):
+ data = (serial.encode() + secret)
+ digest = sha1(data).digest()[-10:]
+ return bytesToRestoreCode(digest)
+
+def getToken(secret, digits=8, seconds=30):
+ """
+ Computes the token for a given secret
+ Returns the token, and the seconds remaining
+ for that token
+ """
+ from struct import pack, unpack
+ t = int(time())
+ msg = pack(">Q", int(t / seconds))
+ r = hmac.new(secret, msg, sha1).digest()
+ k = r[19]
+
+ # Python2 compat
+ if isinstance(k, str):
+ k = ord(k)
+
+ idx = k & 0x0f
+ h = unpack(">L", r[idx:idx+4])[0] & 0x7fffffff
+ return h % (10 ** digits), -(t % seconds - seconds)
+
+
+def normalizeSerial(serial):
+ """
+ Normalizes a serial
+ Will uppercase it, remove its dashes and strip
+ any whitespace
+ """
+ return serial.upper().replace("-", "").strip()
+
+def prettifySerial(serial):
+ """
+ Returns the prettified version of a serial
+ It should look like XX-AAAA-BBBB-CCCC-DDDD
+ """
+ serial = normalizeSerial(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]))
+
+
+# restore functions, as reverse-engineered from the android implementation
+
+def restore(serial, code):
+ serial = normalizeSerial(serial)
+ if len(code) != 10:
+ raise ValueError("invalid restore code (should be 10 bytes): %r" % (code))
+
+ challenge = initiatePaperRestore(serial)
+ if len(challenge) != 32:
+ raise HTTPError("Invalid challenge length (expected 32, got %i)" % (len(challenge)))
+
+ code = restoreCodeToBytes(code)
+ hash = hmac.new(code, serial.encode() + challenge, digestmod=sha1).digest()
+
+ otp = getOneTimePad(20)
+ e = encrypt(hash + otp)
+ response = validatePaperRestore(serial + e)
+ secret = decrypt(response, otp)
+
+ return secret
+
+def restoreCodeToBytes(code):
+ ret = bytearray()
+ for c in code:
+ c = ord(c)
+ if 58 > c > 47:
+ c -= 48
+ else:
+ mod = c - 55
+ if c > 72:
+ mod -= 1
+ if c > 75:
+ mod -= 1
+ if c > 78:
+ mod -= 1
+ if c > 82:
+ mod -= 1
+ c = mod
+ ret.append(c)
+
+ return bytes(ret)
+
+def initiatePaperRestore(serial, host=ENROLL_HOSTS["default"], path="/enrollment/initiatePaperRestore.htm"):
+ return getServerResponse(serial, host, path)
+
+def validatePaperRestore(data, host=ENROLL_HOSTS["default"], path="/enrollment/validatePaperRestore.htm"):
+ try:
+ response = getServerResponse(data, host, path)
+ except HTTPError as e:
+ if e.response.status == 600:
+ raise HTTPError("Invalid serial or restore key", e.response)
+ else:
+ raise
+ return response
Submodule python-bna added at 034a56
Oops, something went wrong.

0 comments on commit b3be10f

Please sign in to comment.