#!/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 "'qlmanage' failed, maybe binary is not available" if not system("/usr/bin/qlmanage", "-p", img.path) else STDERR.puts "'qrencode' failed, maybe binary is not available?" end