#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# androidkindlekey.py
# Copyright © 2010-22 by Thom, Apprentice Harper et al.

# 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
#  2.0   - Python 3 compatibility
#  2.1   - Remove OpenSSL support; only support PyCryptodome

"""
Retrieve Kindle for Android Serial Number.
"""

__license__ = 'GPL v3'
__version__ = '2.1'

import os
import sys
import traceback
import getopt
import tempfile
import zlib
import tarfile
from hashlib import md5
from io import BytesIO
from binascii import a2b_hex, b2a_hex

try:
    from Cryptodome.Cipher import AES, DES
except ImportError:
    from Crypto.Cipher import AES, DES

# Routines common to Mac and PC


class DrmException(Exception):
    pass

STORAGE  = "backup.ab"
STORAGE1 = "AmazonSecureStorage.xml"
STORAGE2 = "map_data_storage.db"


def unpad(data, padding=16):
    if sys.version_info[0] == 2:
        pad_len = ord(data[-1])
    else:
        pad_len = data[-1]

    return data[:-pad_len]

def pad(data, padding_len=16):
    padding_data_len = padding_len - (len(data) % padding_len)
    plaintext = data + chr(padding_data_len) * padding_data_len
    return plaintext

class AndroidObfuscation(object):
    '''AndroidObfuscation
    For the key, it's written in java, and run in android dalvikvm
    '''

    key = a2b_hex('0176e04c9408b1702d90be333fd53523')

    def _get_cipher(self):
        return AES.new(self.key, AES.MODE_ECB)

    def encrypt(self, plaintext):
        pt = pad(plaintext.encode('utf-8'), 16)
        return b2a_hex(self._get_cipher().encrypt(pt))

    def decrypt(self, ciphertext):
        ct = a2b_hex(ciphertext)
        return unpad(self._get_cipher().decrypt(ct), 16)

class AndroidObfuscationV2(AndroidObfuscation):
    '''AndroidObfuscationV2
    '''

    count = 503
    password = b'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):
        return DES.new(self.key, DES.MODE_CBC, self.iv)

def parse_preference(path):
    ''' parse android's shared preference xml '''
    storage = {}
    read = open(path)
    for line in read:
        line = line.strip()
        # <string name="key">value</string>
        if line.startswith('<string name="'):
            index = line.find('"', 14)
            key = line[14:index]
            value = line[index+2:-9]
            storage[key] = value
    read.close()
    return storage

def get_serials1(path=STORAGE1):
    ''' get serials from android's shared preference xml '''

    if not os.path.isfile(path):
        return []

    storage = parse_preference(path)
    salt = storage.get('AmazonSaltKey')
    if salt and len(salt) == 16:
        obfuscation = AndroidObfuscationV2(a2b_hex(salt))
    else:
        obfuscation = AndroidObfuscation()

    def get_value(key):
        encrypted_key = obfuscation.encrypt(key)
        encrypted_value = storage.get(encrypted_key)
        if encrypted_value:
            return obfuscation.decrypt(encrypted_value)
        return ''

    # also see getK4Pids in kgenpids.py
    try:
        dsnid = get_value('DsnId')
    except:
        sys.stderr.write('cannot get DsnId\n')
        return []

    try:
        tokens = set(get_value('kindle.account.tokens').split(','))
    except:
        sys.stderr.write('cannot get kindle account tokens\n')
        return []

    serials = []
    if dsnid:
        serials.append(dsnid)
    for token in tokens:
        if token:
            serials.append('%s%s' % (dsnid, token))
            serials.append(token)
    return serials

def get_serials2(path=STORAGE2):
    ''' get serials from android's sql database '''
    if not os.path.isfile(path):
        return []

    import sqlite3
    connection = sqlite3.connect(path)
    cursor = connection.cursor()
    cursor.execute('''select device_data_value from device_data where device_data_key like '%serial.number%' ''')
    device_data_keys = cursor.fetchall()
    dsns = []
    for device_data_row in device_data_keys:
        try:
            if device_data_row and device_data_row[0]:
                if len(device_data_row[0]) > 0:
                    dsns.append(device_data_row[0])
        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]:
                if len(userdata_row[0]) > 0:
                    if ',' in userdata_row[0]:
                        splits = userdata_row[0].split(',')
                        for split in splits:
                            tokens.append(split)
                    tokens.append(userdata_row[0])
        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(y)
            serials.append(x+y)

    connection.close()

    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] == b'ANDROID BACKUP':
            output = BytesIO(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 open(outfile, 'w') as keyfileout:
            for key in keys:
                keyfileout.write(key)
                keyfileout.write("\n")
        return True
    return False


def usage(progname):
    print("Decrypts the serial number(s) of Kindle For Android from Android backup or file")
    print("Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+.")
    print("Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml")
    print("Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db")
    print("")
    print("Usage:")
    print("    {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname))


def cli_main():
    argv=sys.argv
    progname = os.path.basename(argv[0])
    print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))

    try:
        opts, args = getopt.getopt(argv[1:], "hb:")
    except getopt.GetoptError as err:
        usage(progname)
        print("\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("\n{0:s} file not found".format(inpath))
        return 2

    if getkey(outfile, inpath):
        print("\nSaved Kindle for Android key to {0}".format(outfile))
    else:
        print("\nCould not retrieve Kindle for Android key.")
    return 0


def gui_main():
    try:
        import tkinter
        import tkinter.constants
        import tkinter.messagebox
        import tkinter.filedialog
    except:
        print("tkinter not installed")
        return 0

    class DecryptionDialog(tkinter.Frame):
        def __init__(self, root):
            tkinter.Frame.__init__(self, root, border=5)
            self.status = tkinter.Label(self, text="Select backup.ab file")
            self.status.pack(fill=tkinter.constants.X, expand=1)
            body = tkinter.Frame(self)
            body.pack(fill=tkinter.constants.X, expand=1)
            sticky = tkinter.constants.E + tkinter.constants.W
            body.grid_columnconfigure(1, weight=2)
            tkinter.Label(body, text="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, "backup.ab")
            button = tkinter.Button(body, text="...", command=self.get_keypath)
            button.grid(row=0, column=2)
            buttons = tkinter.Frame(self)
            buttons.pack()
            button2 = tkinter.Button(
                buttons, text="Extract", width=10, command=self.generate)
            button2.pack(side=tkinter.constants.LEFT)
            tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
            button3 = tkinter.Button(
                buttons, text="Quit", width=10, command=self.quit)
            button3.pack(side=tkinter.constants.RIGHT)

        def get_keypath(self):
            keypath = tkinter.filedialog.askopenfilename(
                parent=None, title="Select backup.ab file",
                defaultextension=".ab",
                filetypes=[('adb backup com.amazon.kindle', '.ab'),
                           ('All Files', '.*')])
            if keypath:
                keypath = os.path.normpath(keypath)
                self.keypath.delete(0, tkinter.constants.END)
                self.keypath.insert(0, keypath)
            return

        def generate(self):
            inpath = self.keypath.get()
            self.status['text'] = "Getting key..."
            try:
                keys = get_serials(inpath)
                keycount = 0
                for key in keys:
                    while True:
                        keycount += 1
                        outfile = os.path.join(progpath,"kindlekey{0:d}.k4a".format(keycount))
                        if not os.path.exists(outfile):
                            break

                    with open(outfile, 'w') as keyfileout:
                        keyfileout.write(key)
                    success = True
                    tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
            except Exception as e:
                self.status['text'] = "Error: {0}".format(e.args[0])
                return
            self.status['text'] = "Select backup.ab file"

    argv=sys.argv()
    progpath, progname = os.path.split(argv[0])
    root = tkinter.Tk()
    root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
    root.resizable(True, False)
    root.minsize(300, 0)
    DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
    root.mainloop()
    return 0

if __name__ == '__main__':
    if len(sys.argv) > 1:
        sys.exit(cli_main())
    sys.exit(gui_main())
