#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import with_statement # androidkindlekey.py # Copyright © 2013-15 by Thom and Apprentice Harper # Some portions Copyright © 2010-15 by some_updates and Apprentice Alf # # Revision history: # 1.0 - AmazonSecureStorage.xml decryption to serial number # 1.1 - map_data_storage.db decryption to serial number # 1.2 - Changed to be callable from AppleScript by returning only serial number # - and changed name to androidkindlekey.py # - and added in unicode command line support # 1.3 - added in TkInter interface, output to a file # 1.4 - Fix some problems identified by Aldo Bleeker # 1.5 - Fix another problem identified by Aldo Bleeker """ Retrieve Kindle for Android Serial Number. """ from __future__ import print_function __license__ = 'GPL v3' __version__ = '1.5' import os import sys import traceback import getopt import tempfile import zlib import tarfile from hashlib import md5 from cStringIO import StringIO from binascii import a2b_hex, b2a_hex # Routines common to Mac and PC # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data,unicode): data = data.encode(self.encoding,"replace") self.stream.write(data) self.stream.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return [u"kindlekey.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: argvencoding = "utf-8" return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] class DrmException(Exception): pass STORAGE = u"backup.ab" STORAGE1 = u"AmazonSecureStorage.xml" STORAGE2 = u"map_data_storage.db" class AndroidObfuscation(object): '''AndroidObfuscation For the key, it's written in java, and run in android dalvikvm ''' key = a2b_hex('0176e04c9408b1702d90be333fd53523') def encrypt(self, plaintext): cipher = self._get_cipher() padding = len(self.key) - len(plaintext) % len(self.key) plaintext += chr(padding) * padding return b2a_hex(cipher.encrypt(plaintext)) def decrypt(self, ciphertext): cipher = self._get_cipher() plaintext = cipher.decrypt(a2b_hex(ciphertext)) return plaintext[:-ord(plaintext[-1])] def _get_cipher(self): try: from Crypto.Cipher import AES return AES.new(self.key) except ImportError: from aescbc import AES, noPadding return AES(self.key, padding=noPadding()) class AndroidObfuscationV2(AndroidObfuscation): '''AndroidObfuscationV2 ''' count = 503 password = 'Thomsun was here!' def __init__(self, salt): key = self.password + salt for _ in range(self.count): key = md5(key).digest() self.key = key[:8] self.iv = key[8:16] def _get_cipher(self): try : from Crypto.Cipher import DES return DES.new(self.key, DES.MODE_CBC, self.iv) except ImportError: from python_des import Des, CBC return Des(self.key, CBC, self.iv) def parse_preference(path): ''' parse android's shared preference xml ''' storage = {} read = open(path) for line in read: line = line.strip() # value if line.startswith(' 0: dsns.append(userdata_utf8) except: print("Error getting one of the device serial name keys") traceback.print_exc() pass dsns = list(set(dsns)) cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') userdata_keys = cursor.fetchall() tokens = [] for userdata_row in userdata_keys: try: if userdata_row and userdata_row[0]: userdata_utf8 = userdata_row[0].encode('utf8') if len(userdata_utf8) > 0: tokens.append(userdata_utf8) except: print("Error getting one of the account token keys") traceback.print_exc() pass tokens = list(set(tokens)) serials = [] for x in dsns: serials.append(x) for y in tokens: serials.append('%s%s' % (x, y)) for y in tokens: serials.append(y) return serials def get_serials(path=STORAGE): '''get serials from files in from android backup.ab backup.ab can be get using adb command: shell> adb backup com.amazon.kindle or from individual files if they're passed. ''' if not os.path.isfile(path): return [] basename = os.path.basename(path) if basename == STORAGE1: return get_serials1(path) elif basename == STORAGE2: return get_serials2(path) output = None try : read = open(path, 'rb') head = read.read(24) if head[:14] == 'ANDROID BACKUP': output = StringIO(zlib.decompress(read.read())) except Exception: pass finally: read.close() if not output: return [] serials = [] tar = tarfile.open(fileobj=output) for member in tar.getmembers(): if member.name.strip().endswith(STORAGE1): write = tempfile.NamedTemporaryFile(mode='wb', delete=False) write.write(tar.extractfile(member).read()) write.close() write_path = os.path.abspath(write.name) serials.extend(get_serials1(write_path)) os.remove(write_path) elif member.name.strip().endswith(STORAGE2): write = tempfile.NamedTemporaryFile(mode='wb', delete=False) write.write(tar.extractfile(member).read()) write.close() write_path = os.path.abspath(write.name) serials.extend(get_serials2(write_path)) os.remove(write_path) return list(set(serials)) __all__ = [ 'get_serials', 'getkey'] # procedure for CLI and GUI interfaces # returns single or multiple keys (one per line) in the specified file def getkey(outfile, inpath): keys = get_serials(inpath) if len(keys) > 0: with file(outfile, 'w') as keyfileout: for key in keys: keyfileout.write(key) keyfileout.write("\n") return True return False def usage(progname): print(u"Decrypts the serial number(s) of Kindle For Android from Android backup or file") print(u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+.") print(u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml") print(u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db") print(u"") print(u"Usage:") print(u" {0:s} [-h] [-b ] []".format(progname)) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) print(u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)) try: opts, args = getopt.getopt(argv[1:], "hb:") except getopt.GetoptError, err: usage(progname) print(u"\nError in options or arguments: {0}".format(err.args[0])) return 2 inpath = "" for o, a in opts: if o == "-h": usage(progname) return 0 if o == "-b": inpath = a if len(args) > 1: usage(progname) return 2 if len(args) == 1: # save to the specified file or directory outfile = args[0] if not os.path.isabs(outfile): outfile = os.path.join(os.path.dirname(argv[0]),outfile) outfile = os.path.abspath(outfile) if os.path.isdir(outfile): outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") else: # save to the same directory as the script outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") # make sure the outpath is OK outfile = os.path.realpath(os.path.normpath(outfile)) if not os.path.isfile(inpath): usage(progname) print(u"\n{0:s} file not found".format(inpath)) return 2 if getkey(outfile, inpath): print(u"\nSaved Kindle for Android key to {0}".format(outfile)) else: print(u"\nCould not retrieve Kindle for Android key.") return 0 def gui_main(): try: import Tkinter import Tkconstants import tkMessageBox import tkFileDialog except: print("Tkinter not installed") return cli_main() class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) self.status = Tkinter.Label(self, text=u"Select backup.ab file") self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0) self.keypath = Tkinter.Entry(body, width=40) self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.insert(2, u"backup.ab") button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button.grid(row=0, column=2) buttons = Tkinter.Frame(self) buttons.pack() button2 = Tkinter.Button( buttons, text=u"Extract", width=10, command=self.generate) button2.pack(side=Tkconstants.LEFT) Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) button3 = Tkinter.Button( buttons, text=u"Quit", width=10, command=self.quit) button3.pack(side=Tkconstants.RIGHT) def get_keypath(self): keypath = tkFileDialog.askopenfilename( parent=None, title=u"Select backup.ab file", defaultextension=u".ab", filetypes=[('adb backup com.amazon.kindle', '.ab'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, Tkconstants.END) self.keypath.insert(0, keypath) return def generate(self): inpath = self.keypath.get() self.status['text'] = u"Getting key..." try: keys = get_serials(inpath) keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount)) if not os.path.exists(outfile): break with file(outfile, 'w') as keyfileout: keyfileout.write(key) success = True tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) except Exception, e: self.status['text'] = u"Error: {0}".format(e.args[0]) return self.status['text'] = u"Select backup.ab file" argv=unicode_argv() progpath, progname = os.path.split(argv[0]) root = Tkinter.Tk() root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main())