Created
January 25, 2014 14:10
-
-
Save stbuehler/8616943 to your computer and use it in GitHub Desktop.
Decode the Battle.net Mobile Authenticator (android) secret data into an otpauth url. Requires a way to access the private data of the application (i.e. a rooted android).
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/ruby | |
# REQUIRES: | |
# * rooted android, as otherwise you can't read the applications private data | |
# * to display the qr code "qrencode" (http://fukuchi.org/works/qrencode/) | |
# and "display" from ImageMagick | |
# This script "decrypts" the token from the internal state of the | |
# Battle.net Mobile Authenticator on android application, converting | |
# it into an "otpauth" url (https://code.google.com/p/google-authenticator/wiki/KeyUriFormat) | |
# and (using qrencode and display) displays it as QR code on the screen | |
# to scan it with any compatible app (FreeOTP for example; Google authenticator doesn't support digits=8) | |
# The decrypted token can either be read manually with root on the device (see below), | |
# or, if the device is attached and debug mode enabled, directly read by the script. | |
# Internals: | |
# | |
# The secret token (and serial) for the Battle.net Mobile Authenticator on android is stored | |
# in the file: /data/data/com.blizzard.bma/shared_prefs/com.blizzard.bma.AUTH_STORE.xml | |
# in the property "com.blizzard.bma.AUTH_STORE.HASH": | |
# | |
# <?xml version='1.0' encoding='utf-8' standalone='yes' ?> | |
# <map> | |
# <long name="com.blizzard.bma.AUTH_STORE.CLOCK_OFFSET" value="[clock offset]" /> | |
# <int name="com.blizzard.bma.AUTH_STORE_HASH_VERSION" value="10" /> | |
# <string name="com.blizzard.bma.AUTH_STORE.HASH">["encrypted" token]</string> | |
# <long name="com.blizzard.bma.AUTH_STORE.LAST_MODIFIED" value="[timestamp]" /> | |
# </map> | |
# The encrypted token is a hex string, encoding 57 bytes; decode it into a byte array, decrypt it with | |
# the xor "mask". The decrypted token now consists of 40 bytes hex-encoding the secret and 17 bytes with | |
# the serial (US|EU)-\d{4}-\d{4}-\d{4} | |
# The hex-decoded secret can be used with TOTP (RFC 6238; X = 30, T0 = 0, digit = 8) to generate | |
# the authentication codes. | |
def base32(str) | |
cDIGITS = ('A'..'Z').to_a + ('2'..'7').to_a | |
dMASK = 0x1f | |
cSHIFT = 5 | |
bytes = str.unpack('C*') | |
paddedLen = 8 * ((bytes.length + 4)/5) | |
bits = 0 | |
haveBits = 0 | |
b32 = [] | |
bytes.each do |byte| | |
bits = (bits << 8) | byte | |
haveBits += 8 | |
while haveBits >= cSHIFT | |
b32 << cDIGITS[dMASK & (bits >> (haveBits - cSHIFT))] | |
haveBits -= cSHIFT | |
end | |
bits &= dMASK | |
end | |
if haveBits > 0 | |
b32 << cDIGITS[dMASK & (bits << (cSHIFT - haveBits))] | |
end | |
b32.join + "=" * (paddedLen - b32.length) | |
end | |
def otpauth(serial, token) | |
"otpauth://totp/#{serial}:#{serial}?secret=#{base32(token)}&issuer=#{serial}&digits=8" | |
end | |
mask = [57,142,39,252,80,39,106,101,96,101,176,229,37,244,192,108,4,198,16,117,40,107,142,122,237,165,157,169,129,59,93,214,200,13,47,179,128,104,119,63,165,155,164,124,23,202,108,100,121,1,92,29,91,139,143,107,154] | |
STDOUT.puts "Enter encrypted token (or press enter to read with adb shell): " | |
token = STDIN.readline.strip | |
if token.length == 0 | |
IO.popen(["adb", "shell", "cat", "/data/data/com.blizzard.bma/shared_prefs/com.blizzard.bma.AUTH_STORE.xml"], "r") do |i| | |
i.readlines.each do |line| | |
m = /<string name="com.blizzard.bma.AUTH_STORE.HASH">(.*)<\/string>/.match(line) | |
token = m[1] if m | |
end | |
end | |
end | |
token = [token].pack('H*').unpack('C*').zip(mask).map { |a,b| a ^ b }.pack('C*') | |
serial = token[40..-1] | |
token = [token[0..39]].pack('H*') | |
otpurl = otpauth(serial, token) | |
puts otpurl | |
require 'tempfile' | |
img = Tempfile.new(['otpauth_qr', '.png']) | |
if system("qrencode", "-s", "5", "-o", img.path, otpurl) | |
puts "press escape to exit 'display'" | |
STDERR.puts "'display' failed, maybe binary is not available" if not system("display", img.path) | |
else | |
STDERR.puts "'qrencode' failed, maybe binary is not available?" | |
end |
It works perfectly fine, just requieres packages:
ruby imagemagick qrencode
Awesome! Great job.
Thank you very much!
This is a python library which can directly generate seeds from the Battle.net API, and dump the secret/otpauth QR url:
@jleclanche Thanks for developing that for us!
great script, thanks
@jleclanche You're a godsend! Thank you as well, @stbuehler !
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You, sir, deserve a medal!
I'm gonna try this soon. License of this code? Public?
How did you find out how the "encrypted" secret was stored? And which xor mask was to apply?
Cheers!