diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py index 971c02b..213b292 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py @@ -19,7 +19,7 @@ class K4DeDRM(FileTypePlugin): description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.' supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on author = 'DiapDealer, SomeUpdates' # The author of this plugin - version = (0, 3, 1) # The version number of this plugin + version = (0, 3, 5) # The version number of this plugin file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py index 14556db..2eb5376 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py @@ -17,7 +17,7 @@ from __future__ import with_statement # and many many others -__version__ = '3.1' +__version__ = '3.5' class Unbuffered: def __init__(self, stream): diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py index c9d8944..abfc7e4 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py @@ -22,16 +22,16 @@ else: if inCalibre: if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" @@ -218,14 +218,14 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): print "Keys not found in " + kInfoFile return pidlst - # Get the HDD serial - encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) + # Get the ID string used + encodedIDString = encodeHash(GetIDString(),charMap1) # Get the current user name encodedUsername = encodeHash(GetUserName(),charMap1) # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) # Compute the device PID (for which I can tell, is used for nothing). table = generatePidEncryptionTable() diff --git a/Calibre_Plugins/README-Ineptpdf-plugin.txt b/Calibre_Plugins/README-Ineptpdf-plugin.txt index 457adb1..5c4a026 100644 --- a/Calibre_Plugins/README-Ineptpdf-plugin.txt +++ b/Calibre_Plugins/README-Ineptpdf-plugin.txt @@ -8,7 +8,7 @@ This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected wi Installation: -Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptpdf_vXX_plugin.zip) and click the 'Add' button. you're done. +Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_vXX_plugin.zip) and click the 'Add' button. you're done. Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. diff --git a/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt b/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt index 1086890..8725cf4 100644 --- a/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt +++ b/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt @@ -5,7 +5,7 @@ This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If yo This plugin is meant to remove the DRM from .prc, .azw, .azw1, and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books. Installation: -Go to Calibre's Preferences page... click on the Plugins button. Click on the "Add a new plugin" button at the bottom of the screen. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button. Then click on the "Yes" button in the warning dialog that appears. A Confirmation dialog appears that says the plugin has been installed. +Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for official calibre plugins", instead select "Change calibre behavior". Under "Advanced" click on the on the Plugins button. Click on the "Load plugin from file" button at the bottom of the screen. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button. Then click on the "Yes" button in the warning dialog that appears. A Confirmation dialog appears that says the plugin has been installed. Configuration: diff --git a/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt b/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt index ff98316..035547e 100644 --- a/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt +++ b/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt @@ -5,7 +5,7 @@ All credit given to The Dark Reverser for the original standalone script. I had This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files. Calibre can then convert it to whatever format you desire. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower. Installation: -Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (eReaderPDB2PML_vXX_plugin.zip) and click the 'Add' button. You're done. +Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (eReaderPDB2PML_vXX_plugin.zip) and click the 'Add' button. You're done. Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. diff --git a/Calibre_Plugins/README-ignobleepub-plugin.txt b/Calibre_Plugins/README-ignobleepub-plugin.txt index ad52f53..68aa608 100644 --- a/Calibre_Plugins/README-ignobleepub-plugin.txt +++ b/Calibre_Plugins/README-ignobleepub-plugin.txt @@ -9,7 +9,7 @@ with Adobe's Adept encryption. It is meant to function without having to install Installation: -Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and +Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and click the 'Add' button. you're done. Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. diff --git a/Calibre_Plugins/README-ineptepub-plugin.txt b/Calibre_Plugins/README-ineptepub-plugin.txt index 1bc1d7a..9c58cf0 100644 --- a/Calibre_Plugins/README-ineptepub-plugin.txt +++ b/Calibre_Plugins/README-ineptepub-plugin.txt @@ -8,7 +8,7 @@ This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected w Installation: -Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done. +Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Cahnge calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done. Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip index 00782f0..5ca2c27 100644 Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py b/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py index 534c389..7d5130c 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py @@ -1,11 +1,12 @@ -# standlone set of Mac OSX specific routines needed for K4DeDRM +# standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement import sys import os -import subprocess +import os.path +import subprocess from struct import pack, unpack, unpack_from class DrmException(Exception): @@ -68,11 +69,9 @@ def _load_crypto_libcrypto(): raise DrmException('AES decryption failed') return out.raw - def keyivgen(self, passwd, salt): + def keyivgen(self, passwd, salt, iter, keylen): saltlen = len(salt) passlen = len(passwd) - iter = 0x3e8 - keylen = 80 out = create_string_buffer(keylen) rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) return out.raw @@ -114,9 +113,10 @@ def SHA256(message): charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" -# For Future Reference from .kinf approach of K4PC -charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - +# For kinf approach of K4PC/K4Mac +# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# For Mac they seem to re-use charMap2 here +charMap5 = charMap2 def encode(data, map): result = "" @@ -144,7 +144,7 @@ def decode(data,map): result += pack("B",value) return result -# For Future Reference from .kinf approach of K4PC +# For .kinf approach of K4PC and now K4Mac # generate table of prime number less than or equal to int n def primes(n): if n==2: return [2] @@ -166,7 +166,6 @@ def primes(n): return [2]+[x for x in s if x] - # uses a sub process to get the Hard Drive Serial Number using ioreg # returns with the serial number of drive whose BSD Name is "disk0" def GetVolumeSerialNumber(): @@ -196,24 +195,217 @@ def GetVolumeSerialNumber(): foundIt = True break if not foundIt: - sernum = '9999999999' + sernum = '' return sernum +def GetUserHomeAppSupKindleDirParitionName(): + home = os.getenv('HOME') + dpath = home + '/Library/Application Support/Kindle' + cmdline = '/sbin/mount' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + disk = '' + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + if resline.startswith('/dev'): + (devpart, mpath) = resline.split(' on ') + dpart = devpart[5:] + pp = mpath.find('(') + if pp >= 0: + mpath = mpath[:pp-1] + if dpath.startswith(mpath): + disk = dpart + return disk + +# uses a sub process to get the UUID of the specified disk partition using ioreg +def GetDiskPartitionUUID(diskpart): + uuidnum = os.getenv('MYUUIDNUMBER') + if uuidnum != None: + return uuidnum + cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + uuidnum = None + foundIt = False + nest = 0 + uuidnest = -1 + partnest = -2 + for j in xrange(cnt): + resline = reslst[j] + if resline.find('{') >= 0: + nest += 1 + if resline.find('}') >= 0: + nest -= 1 + pp = resline.find('"UUID" = "') + if pp >= 0: + uuidnum = resline[pp+10:-1] + uuidnum = uuidnum.strip() + uuidnest = nest + if partnest == uuidnest and uuidnest > 0: + foundIt = True + break + bb = resline.find('"BSD Name" = "') + if bb >= 0: + bsdname = resline[bb+14:-1] + bsdname = bsdname.strip() + if (bsdname == diskpart): + partnest = nest + else : + partnest = -2 + if partnest == uuidnest and partnest > 0: + foundIt = True + break + if nest == 0: + partnest = -2 + uuidnest = -1 + uuidnum = None + bsdname = None + if not foundIt: + uuidnum = '' + return uuidnum + +def GetMACAddressMunged(): + macnum = os.getenv('MYMACNUM') + if macnum != None: + return macnum + cmdline = '/sbin/ifconfig en0' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + macnum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('ether ') + if pp >= 0: + macnum = resline[pp+6:-1] + macnum = macnum.strip() + # print "original mac", macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 + maclst = macnum.split(':') + n = len(maclst) + if n != 6: + fountIt = False + break + for i in range(6): + maclst[i] = int('0x' + maclst[i], 0) + mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + mlst[5] = maclst[5] ^ 0xa5 + mlst[4] = maclst[3] ^ 0xa5 + mlst[3] = maclst[4] ^ 0xa5 + mlst[2] = maclst[2] ^ 0xa5 + mlst[1] = maclst[1] ^ 0xa5 + mlst[0] = maclst[0] ^ 0xa5 + macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + foundIt = True + break + if not foundIt: + macnum = '' + return macnum + + # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') return username + # implements an Pseudo Mac Version of Windows built-in Crypto routine -def CryptUnprotectData(encryptedData, salt): - sp = GetVolumeSerialNumber() + '!@#' + GetUserName() +# used by Kindle for Mac versions < 1.6.0 +def CryptUnprotectData(encryptedData): + sernum = GetVolumeSerialNumber() + if sernum == '': + sernum = '9999999999' + sp = sernum + '!@#' + GetUserName() passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + iter = 0x3e8 + keylen = 0x80 crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt) + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] crp.set_decrypt_key(key,iv) cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + +def isNewInstall(): + home = os.getenv('HOME') + # soccer game fan anyone + dpath = home + '/Library/Application Support/Kindle/storage/.pes2011' + # print dpath, os.path.exists(dpath) + if os.path.exists(dpath): + return True + return False + + +def GetIDString(): + # K4Mac now has an extensive set of ids strings it uses + # in encoding pids and in creating unique passwords + # for use in its own version of CryptUnprotectDataV2 + + # BUT Amazon has now become nasty enough to detect when its app + # is being run under a debugger and actually changes code paths + # including which one of these strings is chosen, all to try + # to prevent reverse engineering + + # Sad really ... they will only hurt their own sales ... + # true book lovers really want to keep their books forever + # and move them to their devices and DRM prevents that so they + # will just buy from someplace else that they can remove + # the DRM from + + # Amazon should know by now that true book lover's are not like + # penniless kids that pirate music, we do not pirate books + + if isNewInstall(): + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + sernum = GetVolumeSerialNumber() + if len(sernum) > 7: + return sernum + diskpart = GetUserHomeAppSupKindleDirParitionName() + uuidnum = GetDiskPartitionUUID(diskpart) + if len(uuidnum) > 7: + return uuidnum + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + return '9999999999' + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.6.0 +def CryptUnprotectDataV2(encryptedData): + sp = GetUserName() + ':&%:' + GetIDString() + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) return cleartext @@ -232,18 +424,16 @@ def getKindleInfoFiles(kInfoFiles): if os.path.isfile(resline): kInfoFiles.append(resline) found = True - # For Future Reference - # - # # add any .kinf files - # cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"' - # cmdline = cmdline.encode(sys.getfilesystemencoding()) - # p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - # out1, out2 = p1.communicate() - # reslst = out1.split('\n') - # for resline in reslst: - # if os.path.isfile(resline): - # kInfoFiles.append(resline) - # found = True + # add any .kinf files + cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True if not found: print('No kindle-info files have been found.') return kInfoFiles @@ -251,7 +441,7 @@ def getKindleInfoFiles(kInfoFiles): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -259,7 +449,6 @@ def getDBfromFile(kInfoFile): data = infoReader.read() if data.find('[') != -1 : - # older style kindle-info file items = data.split('[') for item in items: @@ -273,84 +462,89 @@ def getDBfromFile(kInfoFile): if keyname == "unknown": keyname = keyhash encryptedValue = decode(rawdata,charMap2) - salt = '16743' - cleartext = CryptUnprotectData(encryptedValue, salt) - DB[keyname] = decode(cleartext,charMap1) + cleartext = CryptUnprotectData(encryptedValue) + DB[keyname] = cleartext cnt = cnt + 1 if cnt == 0: DB = None return DB - # For Future Reference taken from K4PC 1.5.0 .kinf - # - # # else newer style .kinf file - # # the .kinf file uses "/" to separate it into records - # # so remove the trailing "/" to make it easy to use split - # data = data[:-1] - # items = data.split('/') - # - # # loop through the item records until all are processed - # while len(items) > 0: - # - # # get the first item record - # item = items.pop(0) - # - # # the first 32 chars of the first record of a group - # # is the MD5 hash of the key name encoded by charMap5 - # keyhash = item[0:32] - # - # # the raw keyhash string is also used to create entropy for the actual - # # CryptProtectData Blob that represents that keys contents - # entropy = SHA1(keyhash) - # - # # the remainder of the first record when decoded with charMap5 - # # has the ':' split char followed by the string representation - # # of the number of records that follow - # # and make up the contents - # srcnt = decode(item[34:],charMap5) - # rcnt = int(srcnt) - # - # # read and store in rcnt records of data - # # that make up the contents value - # edlst = [] - # for i in xrange(rcnt): - # item = items.pop(0) - # edlst.append(item) - # - # keyname = "unknown" - # for name in names: - # if encodeHash(name,charMap5) == keyhash: - # keyname = name - # break - # if keyname == "unknown": - # keyname = keyhash - # - # # the charMap5 encoded contents data has had a length - # # of chars (always odd) cut off of the front and moved - # # to the end to prevent decoding using charMap5 from - # # working properly, and thereby preventing the ensuing - # # CryptUnprotectData call from succeeding. - # - # # The offset into the charMap5 encoded contents seems to be: - # # len(contents) - largest prime number less than or equal to int(len(content)/3) - # # (in other words split "about" 2/3rds of the way through) - # - # # move first offsets chars to end to align for decode by charMap5 - # encdata = "".join(edlst) - # contlen = len(encdata) - # noffset = contlen - primes(int(contlen/3))[-1] - # - # # now properly split and recombine - # # by moving noffset chars from the start of the - # # string to the end of the string - # pfx = encdata[0:noffset] - # encdata = encdata[noffset:] - # encdata = encdata + pfx - # - # # decode using Map5 to get the CryptProtect Data - # encryptedValue = decode(encdata,charMap5) - # DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - # cnt = cnt + 1 + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # "entropy" not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = CryptUnprotectDataV2(encryptedValue) + # Debugging + # print keyname + # print cleartext + # print cleartext.encode('hex') + # print + DB[keyname] = cleartext + cnt = cnt + 1 if cnt == 0: DB = None diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py b/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py index 690033b..6acdd5c 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py @@ -122,6 +122,9 @@ def GetVolumeSerialNumber(): return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() +def GetIDString(): + return GetVolumeSerialNumber() + def getLastError(): GetLastError = kernel32.GetLastError GetLastError.argtypes = None @@ -181,18 +184,27 @@ def getKindleInfoFiles(kInfoFiles): kInfoFiles.append(kinfopath) # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' if not os.path.isfile(kinfopath): - print('No .kinf files have not been found.') + print('No K4PC 1.5.X .kinf files have not been found.') else: kInfoFiles.append(kinfopath) + + # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' + if not os.path.isfile(kinfopath): + print('No K4PC 1.6.X .kinf files have not been found.') + else: + kInfoFiles.append(kinfopath) + return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index aecb257..4783bb8 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,7 +24,7 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM 2.7, Written 2010–2011 by Apprentice Alf and others. + DeDRM 2.8, Written 2010–2011 by Apprentice Alf and others. CFBundleIconFile droplet CFBundleInfoDictionaryVersion @@ -34,7 +34,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.7 + 2.8 CFBundleSignature dplt LSMinimumSystemVersion diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index 14556db..2eb5376 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -17,7 +17,7 @@ from __future__ import with_statement # and many many others -__version__ = '3.1' +__version__ = '3.5' class Unbuffered: def __init__(self, stream): diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py index 534c389..7d5130c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py @@ -1,11 +1,12 @@ -# standlone set of Mac OSX specific routines needed for K4DeDRM +# standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement import sys import os -import subprocess +import os.path +import subprocess from struct import pack, unpack, unpack_from class DrmException(Exception): @@ -68,11 +69,9 @@ def _load_crypto_libcrypto(): raise DrmException('AES decryption failed') return out.raw - def keyivgen(self, passwd, salt): + def keyivgen(self, passwd, salt, iter, keylen): saltlen = len(salt) passlen = len(passwd) - iter = 0x3e8 - keylen = 80 out = create_string_buffer(keylen) rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) return out.raw @@ -114,9 +113,10 @@ def SHA256(message): charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" -# For Future Reference from .kinf approach of K4PC -charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - +# For kinf approach of K4PC/K4Mac +# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# For Mac they seem to re-use charMap2 here +charMap5 = charMap2 def encode(data, map): result = "" @@ -144,7 +144,7 @@ def decode(data,map): result += pack("B",value) return result -# For Future Reference from .kinf approach of K4PC +# For .kinf approach of K4PC and now K4Mac # generate table of prime number less than or equal to int n def primes(n): if n==2: return [2] @@ -166,7 +166,6 @@ def primes(n): return [2]+[x for x in s if x] - # uses a sub process to get the Hard Drive Serial Number using ioreg # returns with the serial number of drive whose BSD Name is "disk0" def GetVolumeSerialNumber(): @@ -196,24 +195,217 @@ def GetVolumeSerialNumber(): foundIt = True break if not foundIt: - sernum = '9999999999' + sernum = '' return sernum +def GetUserHomeAppSupKindleDirParitionName(): + home = os.getenv('HOME') + dpath = home + '/Library/Application Support/Kindle' + cmdline = '/sbin/mount' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + disk = '' + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + if resline.startswith('/dev'): + (devpart, mpath) = resline.split(' on ') + dpart = devpart[5:] + pp = mpath.find('(') + if pp >= 0: + mpath = mpath[:pp-1] + if dpath.startswith(mpath): + disk = dpart + return disk + +# uses a sub process to get the UUID of the specified disk partition using ioreg +def GetDiskPartitionUUID(diskpart): + uuidnum = os.getenv('MYUUIDNUMBER') + if uuidnum != None: + return uuidnum + cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + uuidnum = None + foundIt = False + nest = 0 + uuidnest = -1 + partnest = -2 + for j in xrange(cnt): + resline = reslst[j] + if resline.find('{') >= 0: + nest += 1 + if resline.find('}') >= 0: + nest -= 1 + pp = resline.find('"UUID" = "') + if pp >= 0: + uuidnum = resline[pp+10:-1] + uuidnum = uuidnum.strip() + uuidnest = nest + if partnest == uuidnest and uuidnest > 0: + foundIt = True + break + bb = resline.find('"BSD Name" = "') + if bb >= 0: + bsdname = resline[bb+14:-1] + bsdname = bsdname.strip() + if (bsdname == diskpart): + partnest = nest + else : + partnest = -2 + if partnest == uuidnest and partnest > 0: + foundIt = True + break + if nest == 0: + partnest = -2 + uuidnest = -1 + uuidnum = None + bsdname = None + if not foundIt: + uuidnum = '' + return uuidnum + +def GetMACAddressMunged(): + macnum = os.getenv('MYMACNUM') + if macnum != None: + return macnum + cmdline = '/sbin/ifconfig en0' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + macnum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('ether ') + if pp >= 0: + macnum = resline[pp+6:-1] + macnum = macnum.strip() + # print "original mac", macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 + maclst = macnum.split(':') + n = len(maclst) + if n != 6: + fountIt = False + break + for i in range(6): + maclst[i] = int('0x' + maclst[i], 0) + mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + mlst[5] = maclst[5] ^ 0xa5 + mlst[4] = maclst[3] ^ 0xa5 + mlst[3] = maclst[4] ^ 0xa5 + mlst[2] = maclst[2] ^ 0xa5 + mlst[1] = maclst[1] ^ 0xa5 + mlst[0] = maclst[0] ^ 0xa5 + macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + foundIt = True + break + if not foundIt: + macnum = '' + return macnum + + # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') return username + # implements an Pseudo Mac Version of Windows built-in Crypto routine -def CryptUnprotectData(encryptedData, salt): - sp = GetVolumeSerialNumber() + '!@#' + GetUserName() +# used by Kindle for Mac versions < 1.6.0 +def CryptUnprotectData(encryptedData): + sernum = GetVolumeSerialNumber() + if sernum == '': + sernum = '9999999999' + sp = sernum + '!@#' + GetUserName() passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + iter = 0x3e8 + keylen = 0x80 crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt) + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] crp.set_decrypt_key(key,iv) cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + +def isNewInstall(): + home = os.getenv('HOME') + # soccer game fan anyone + dpath = home + '/Library/Application Support/Kindle/storage/.pes2011' + # print dpath, os.path.exists(dpath) + if os.path.exists(dpath): + return True + return False + + +def GetIDString(): + # K4Mac now has an extensive set of ids strings it uses + # in encoding pids and in creating unique passwords + # for use in its own version of CryptUnprotectDataV2 + + # BUT Amazon has now become nasty enough to detect when its app + # is being run under a debugger and actually changes code paths + # including which one of these strings is chosen, all to try + # to prevent reverse engineering + + # Sad really ... they will only hurt their own sales ... + # true book lovers really want to keep their books forever + # and move them to their devices and DRM prevents that so they + # will just buy from someplace else that they can remove + # the DRM from + + # Amazon should know by now that true book lover's are not like + # penniless kids that pirate music, we do not pirate books + + if isNewInstall(): + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + sernum = GetVolumeSerialNumber() + if len(sernum) > 7: + return sernum + diskpart = GetUserHomeAppSupKindleDirParitionName() + uuidnum = GetDiskPartitionUUID(diskpart) + if len(uuidnum) > 7: + return uuidnum + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + return '9999999999' + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.6.0 +def CryptUnprotectDataV2(encryptedData): + sp = GetUserName() + ':&%:' + GetIDString() + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) return cleartext @@ -232,18 +424,16 @@ def getKindleInfoFiles(kInfoFiles): if os.path.isfile(resline): kInfoFiles.append(resline) found = True - # For Future Reference - # - # # add any .kinf files - # cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"' - # cmdline = cmdline.encode(sys.getfilesystemencoding()) - # p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - # out1, out2 = p1.communicate() - # reslst = out1.split('\n') - # for resline in reslst: - # if os.path.isfile(resline): - # kInfoFiles.append(resline) - # found = True + # add any .kinf files + cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True if not found: print('No kindle-info files have been found.') return kInfoFiles @@ -251,7 +441,7 @@ def getKindleInfoFiles(kInfoFiles): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -259,7 +449,6 @@ def getDBfromFile(kInfoFile): data = infoReader.read() if data.find('[') != -1 : - # older style kindle-info file items = data.split('[') for item in items: @@ -273,84 +462,89 @@ def getDBfromFile(kInfoFile): if keyname == "unknown": keyname = keyhash encryptedValue = decode(rawdata,charMap2) - salt = '16743' - cleartext = CryptUnprotectData(encryptedValue, salt) - DB[keyname] = decode(cleartext,charMap1) + cleartext = CryptUnprotectData(encryptedValue) + DB[keyname] = cleartext cnt = cnt + 1 if cnt == 0: DB = None return DB - # For Future Reference taken from K4PC 1.5.0 .kinf - # - # # else newer style .kinf file - # # the .kinf file uses "/" to separate it into records - # # so remove the trailing "/" to make it easy to use split - # data = data[:-1] - # items = data.split('/') - # - # # loop through the item records until all are processed - # while len(items) > 0: - # - # # get the first item record - # item = items.pop(0) - # - # # the first 32 chars of the first record of a group - # # is the MD5 hash of the key name encoded by charMap5 - # keyhash = item[0:32] - # - # # the raw keyhash string is also used to create entropy for the actual - # # CryptProtectData Blob that represents that keys contents - # entropy = SHA1(keyhash) - # - # # the remainder of the first record when decoded with charMap5 - # # has the ':' split char followed by the string representation - # # of the number of records that follow - # # and make up the contents - # srcnt = decode(item[34:],charMap5) - # rcnt = int(srcnt) - # - # # read and store in rcnt records of data - # # that make up the contents value - # edlst = [] - # for i in xrange(rcnt): - # item = items.pop(0) - # edlst.append(item) - # - # keyname = "unknown" - # for name in names: - # if encodeHash(name,charMap5) == keyhash: - # keyname = name - # break - # if keyname == "unknown": - # keyname = keyhash - # - # # the charMap5 encoded contents data has had a length - # # of chars (always odd) cut off of the front and moved - # # to the end to prevent decoding using charMap5 from - # # working properly, and thereby preventing the ensuing - # # CryptUnprotectData call from succeeding. - # - # # The offset into the charMap5 encoded contents seems to be: - # # len(contents) - largest prime number less than or equal to int(len(content)/3) - # # (in other words split "about" 2/3rds of the way through) - # - # # move first offsets chars to end to align for decode by charMap5 - # encdata = "".join(edlst) - # contlen = len(encdata) - # noffset = contlen - primes(int(contlen/3))[-1] - # - # # now properly split and recombine - # # by moving noffset chars from the start of the - # # string to the end of the string - # pfx = encdata[0:noffset] - # encdata = encdata[noffset:] - # encdata = encdata + pfx - # - # # decode using Map5 to get the CryptProtect Data - # encryptedValue = decode(encdata,charMap5) - # DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - # cnt = cnt + 1 + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # "entropy" not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = CryptUnprotectDataV2(encryptedValue) + # Debugging + # print keyname + # print cleartext + # print cleartext.encode('hex') + # print + DB[keyname] = cleartext + cnt = cnt + 1 if cnt == 0: DB = None diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py index 690033b..6acdd5c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py @@ -122,6 +122,9 @@ def GetVolumeSerialNumber(): return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() +def GetIDString(): + return GetVolumeSerialNumber() + def getLastError(): GetLastError = kernel32.GetLastError GetLastError.argtypes = None @@ -181,18 +184,27 @@ def getKindleInfoFiles(kInfoFiles): kInfoFiles.append(kinfopath) # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' if not os.path.isfile(kinfopath): - print('No .kinf files have not been found.') + print('No K4PC 1.5.X .kinf files have not been found.') else: kInfoFiles.append(kinfopath) + + # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' + if not os.path.isfile(kinfopath): + print('No K4PC 1.6.X .kinf files have not been found.') + else: + kInfoFiles.append(kinfopath) + return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py index c9d8944..abfc7e4 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py @@ -22,16 +22,16 @@ else: if inCalibre: if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" @@ -218,14 +218,14 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): print "Keys not found in " + kInfoFile return pidlst - # Get the HDD serial - encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) + # Get the ID string used + encodedIDString = encodeHash(GetIDString(),charMap1) # Get the current user name encodedUsername = encodeHash(GetUserName(),charMap1) # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) # Compute the device PID (for which I can tell, is used for nothing). table = generatePidEncryptionTable() diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py index 14556db..2eb5376 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py @@ -17,7 +17,7 @@ from __future__ import with_statement # and many many others -__version__ = '3.1' +__version__ = '3.5' class Unbuffered: def __init__(self, stream): diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py index 534c389..7d5130c 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py @@ -1,11 +1,12 @@ -# standlone set of Mac OSX specific routines needed for K4DeDRM +# standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement import sys import os -import subprocess +import os.path +import subprocess from struct import pack, unpack, unpack_from class DrmException(Exception): @@ -68,11 +69,9 @@ def _load_crypto_libcrypto(): raise DrmException('AES decryption failed') return out.raw - def keyivgen(self, passwd, salt): + def keyivgen(self, passwd, salt, iter, keylen): saltlen = len(salt) passlen = len(passwd) - iter = 0x3e8 - keylen = 80 out = create_string_buffer(keylen) rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) return out.raw @@ -114,9 +113,10 @@ def SHA256(message): charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" -# For Future Reference from .kinf approach of K4PC -charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - +# For kinf approach of K4PC/K4Mac +# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# For Mac they seem to re-use charMap2 here +charMap5 = charMap2 def encode(data, map): result = "" @@ -144,7 +144,7 @@ def decode(data,map): result += pack("B",value) return result -# For Future Reference from .kinf approach of K4PC +# For .kinf approach of K4PC and now K4Mac # generate table of prime number less than or equal to int n def primes(n): if n==2: return [2] @@ -166,7 +166,6 @@ def primes(n): return [2]+[x for x in s if x] - # uses a sub process to get the Hard Drive Serial Number using ioreg # returns with the serial number of drive whose BSD Name is "disk0" def GetVolumeSerialNumber(): @@ -196,24 +195,217 @@ def GetVolumeSerialNumber(): foundIt = True break if not foundIt: - sernum = '9999999999' + sernum = '' return sernum +def GetUserHomeAppSupKindleDirParitionName(): + home = os.getenv('HOME') + dpath = home + '/Library/Application Support/Kindle' + cmdline = '/sbin/mount' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + disk = '' + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + if resline.startswith('/dev'): + (devpart, mpath) = resline.split(' on ') + dpart = devpart[5:] + pp = mpath.find('(') + if pp >= 0: + mpath = mpath[:pp-1] + if dpath.startswith(mpath): + disk = dpart + return disk + +# uses a sub process to get the UUID of the specified disk partition using ioreg +def GetDiskPartitionUUID(diskpart): + uuidnum = os.getenv('MYUUIDNUMBER') + if uuidnum != None: + return uuidnum + cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + uuidnum = None + foundIt = False + nest = 0 + uuidnest = -1 + partnest = -2 + for j in xrange(cnt): + resline = reslst[j] + if resline.find('{') >= 0: + nest += 1 + if resline.find('}') >= 0: + nest -= 1 + pp = resline.find('"UUID" = "') + if pp >= 0: + uuidnum = resline[pp+10:-1] + uuidnum = uuidnum.strip() + uuidnest = nest + if partnest == uuidnest and uuidnest > 0: + foundIt = True + break + bb = resline.find('"BSD Name" = "') + if bb >= 0: + bsdname = resline[bb+14:-1] + bsdname = bsdname.strip() + if (bsdname == diskpart): + partnest = nest + else : + partnest = -2 + if partnest == uuidnest and partnest > 0: + foundIt = True + break + if nest == 0: + partnest = -2 + uuidnest = -1 + uuidnum = None + bsdname = None + if not foundIt: + uuidnum = '' + return uuidnum + +def GetMACAddressMunged(): + macnum = os.getenv('MYMACNUM') + if macnum != None: + return macnum + cmdline = '/sbin/ifconfig en0' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + macnum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('ether ') + if pp >= 0: + macnum = resline[pp+6:-1] + macnum = macnum.strip() + # print "original mac", macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 + maclst = macnum.split(':') + n = len(maclst) + if n != 6: + fountIt = False + break + for i in range(6): + maclst[i] = int('0x' + maclst[i], 0) + mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + mlst[5] = maclst[5] ^ 0xa5 + mlst[4] = maclst[3] ^ 0xa5 + mlst[3] = maclst[4] ^ 0xa5 + mlst[2] = maclst[2] ^ 0xa5 + mlst[1] = maclst[1] ^ 0xa5 + mlst[0] = maclst[0] ^ 0xa5 + macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + foundIt = True + break + if not foundIt: + macnum = '' + return macnum + + # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') return username + # implements an Pseudo Mac Version of Windows built-in Crypto routine -def CryptUnprotectData(encryptedData, salt): - sp = GetVolumeSerialNumber() + '!@#' + GetUserName() +# used by Kindle for Mac versions < 1.6.0 +def CryptUnprotectData(encryptedData): + sernum = GetVolumeSerialNumber() + if sernum == '': + sernum = '9999999999' + sp = sernum + '!@#' + GetUserName() passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + iter = 0x3e8 + keylen = 0x80 crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt) + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] crp.set_decrypt_key(key,iv) cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + +def isNewInstall(): + home = os.getenv('HOME') + # soccer game fan anyone + dpath = home + '/Library/Application Support/Kindle/storage/.pes2011' + # print dpath, os.path.exists(dpath) + if os.path.exists(dpath): + return True + return False + + +def GetIDString(): + # K4Mac now has an extensive set of ids strings it uses + # in encoding pids and in creating unique passwords + # for use in its own version of CryptUnprotectDataV2 + + # BUT Amazon has now become nasty enough to detect when its app + # is being run under a debugger and actually changes code paths + # including which one of these strings is chosen, all to try + # to prevent reverse engineering + + # Sad really ... they will only hurt their own sales ... + # true book lovers really want to keep their books forever + # and move them to their devices and DRM prevents that so they + # will just buy from someplace else that they can remove + # the DRM from + + # Amazon should know by now that true book lover's are not like + # penniless kids that pirate music, we do not pirate books + + if isNewInstall(): + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + sernum = GetVolumeSerialNumber() + if len(sernum) > 7: + return sernum + diskpart = GetUserHomeAppSupKindleDirParitionName() + uuidnum = GetDiskPartitionUUID(diskpart) + if len(uuidnum) > 7: + return uuidnum + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + return '9999999999' + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.6.0 +def CryptUnprotectDataV2(encryptedData): + sp = GetUserName() + ':&%:' + GetIDString() + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) return cleartext @@ -232,18 +424,16 @@ def getKindleInfoFiles(kInfoFiles): if os.path.isfile(resline): kInfoFiles.append(resline) found = True - # For Future Reference - # - # # add any .kinf files - # cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"' - # cmdline = cmdline.encode(sys.getfilesystemencoding()) - # p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - # out1, out2 = p1.communicate() - # reslst = out1.split('\n') - # for resline in reslst: - # if os.path.isfile(resline): - # kInfoFiles.append(resline) - # found = True + # add any .kinf files + cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True if not found: print('No kindle-info files have been found.') return kInfoFiles @@ -251,7 +441,7 @@ def getKindleInfoFiles(kInfoFiles): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -259,7 +449,6 @@ def getDBfromFile(kInfoFile): data = infoReader.read() if data.find('[') != -1 : - # older style kindle-info file items = data.split('[') for item in items: @@ -273,84 +462,89 @@ def getDBfromFile(kInfoFile): if keyname == "unknown": keyname = keyhash encryptedValue = decode(rawdata,charMap2) - salt = '16743' - cleartext = CryptUnprotectData(encryptedValue, salt) - DB[keyname] = decode(cleartext,charMap1) + cleartext = CryptUnprotectData(encryptedValue) + DB[keyname] = cleartext cnt = cnt + 1 if cnt == 0: DB = None return DB - # For Future Reference taken from K4PC 1.5.0 .kinf - # - # # else newer style .kinf file - # # the .kinf file uses "/" to separate it into records - # # so remove the trailing "/" to make it easy to use split - # data = data[:-1] - # items = data.split('/') - # - # # loop through the item records until all are processed - # while len(items) > 0: - # - # # get the first item record - # item = items.pop(0) - # - # # the first 32 chars of the first record of a group - # # is the MD5 hash of the key name encoded by charMap5 - # keyhash = item[0:32] - # - # # the raw keyhash string is also used to create entropy for the actual - # # CryptProtectData Blob that represents that keys contents - # entropy = SHA1(keyhash) - # - # # the remainder of the first record when decoded with charMap5 - # # has the ':' split char followed by the string representation - # # of the number of records that follow - # # and make up the contents - # srcnt = decode(item[34:],charMap5) - # rcnt = int(srcnt) - # - # # read and store in rcnt records of data - # # that make up the contents value - # edlst = [] - # for i in xrange(rcnt): - # item = items.pop(0) - # edlst.append(item) - # - # keyname = "unknown" - # for name in names: - # if encodeHash(name,charMap5) == keyhash: - # keyname = name - # break - # if keyname == "unknown": - # keyname = keyhash - # - # # the charMap5 encoded contents data has had a length - # # of chars (always odd) cut off of the front and moved - # # to the end to prevent decoding using charMap5 from - # # working properly, and thereby preventing the ensuing - # # CryptUnprotectData call from succeeding. - # - # # The offset into the charMap5 encoded contents seems to be: - # # len(contents) - largest prime number less than or equal to int(len(content)/3) - # # (in other words split "about" 2/3rds of the way through) - # - # # move first offsets chars to end to align for decode by charMap5 - # encdata = "".join(edlst) - # contlen = len(encdata) - # noffset = contlen - primes(int(contlen/3))[-1] - # - # # now properly split and recombine - # # by moving noffset chars from the start of the - # # string to the end of the string - # pfx = encdata[0:noffset] - # encdata = encdata[noffset:] - # encdata = encdata + pfx - # - # # decode using Map5 to get the CryptProtect Data - # encryptedValue = decode(encdata,charMap5) - # DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - # cnt = cnt + 1 + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # "entropy" not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = CryptUnprotectDataV2(encryptedValue) + # Debugging + # print keyname + # print cleartext + # print cleartext.encode('hex') + # print + DB[keyname] = cleartext + cnt = cnt + 1 if cnt == 0: DB = None diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py index 690033b..6acdd5c 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py @@ -122,6 +122,9 @@ def GetVolumeSerialNumber(): return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() +def GetIDString(): + return GetVolumeSerialNumber() + def getLastError(): GetLastError = kernel32.GetLastError GetLastError.argtypes = None @@ -181,18 +184,27 @@ def getKindleInfoFiles(kInfoFiles): kInfoFiles.append(kinfopath) # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' if not os.path.isfile(kinfopath): - print('No .kinf files have not been found.') + print('No K4PC 1.5.X .kinf files have not been found.') else: kInfoFiles.append(kinfopath) + + # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' + if not os.path.isfile(kinfopath): + print('No K4PC 1.6.X .kinf files have not been found.') + else: + kInfoFiles.append(kinfopath) + return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py index c9d8944..abfc7e4 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py @@ -22,16 +22,16 @@ else: if inCalibre: if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" @@ -218,14 +218,14 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): print "Keys not found in " + kInfoFile return pidlst - # Get the HDD serial - encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) + # Get the ID string used + encodedIDString = encodeHash(GetIDString(),charMap1) # Get the current user name encodedUsername = encodeHash(GetUserName(),charMap1) # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) # Compute the device PID (for which I can tell, is used for nothing). table = generatePidEncryptionTable() diff --git a/KindleBooks/lib/k4mobidedrm.py b/KindleBooks/lib/k4mobidedrm.py index 14556db..2eb5376 100644 --- a/KindleBooks/lib/k4mobidedrm.py +++ b/KindleBooks/lib/k4mobidedrm.py @@ -17,7 +17,7 @@ from __future__ import with_statement # and many many others -__version__ = '3.1' +__version__ = '3.5' class Unbuffered: def __init__(self, stream): diff --git a/KindleBooks/lib/k4mutils.py b/KindleBooks/lib/k4mutils.py index 534c389..7d5130c 100644 --- a/KindleBooks/lib/k4mutils.py +++ b/KindleBooks/lib/k4mutils.py @@ -1,11 +1,12 @@ -# standlone set of Mac OSX specific routines needed for K4DeDRM +# standlone set of Mac OSX specific routines needed for KindleBooks from __future__ import with_statement import sys import os -import subprocess +import os.path +import subprocess from struct import pack, unpack, unpack_from class DrmException(Exception): @@ -68,11 +69,9 @@ def _load_crypto_libcrypto(): raise DrmException('AES decryption failed') return out.raw - def keyivgen(self, passwd, salt): + def keyivgen(self, passwd, salt, iter, keylen): saltlen = len(salt) passlen = len(passwd) - iter = 0x3e8 - keylen = 80 out = create_string_buffer(keylen) rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) return out.raw @@ -114,9 +113,10 @@ def SHA256(message): charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" -# For Future Reference from .kinf approach of K4PC -charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" - +# For kinf approach of K4PC/K4Mac +# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" +# For Mac they seem to re-use charMap2 here +charMap5 = charMap2 def encode(data, map): result = "" @@ -144,7 +144,7 @@ def decode(data,map): result += pack("B",value) return result -# For Future Reference from .kinf approach of K4PC +# For .kinf approach of K4PC and now K4Mac # generate table of prime number less than or equal to int n def primes(n): if n==2: return [2] @@ -166,7 +166,6 @@ def primes(n): return [2]+[x for x in s if x] - # uses a sub process to get the Hard Drive Serial Number using ioreg # returns with the serial number of drive whose BSD Name is "disk0" def GetVolumeSerialNumber(): @@ -196,24 +195,217 @@ def GetVolumeSerialNumber(): foundIt = True break if not foundIt: - sernum = '9999999999' + sernum = '' return sernum +def GetUserHomeAppSupKindleDirParitionName(): + home = os.getenv('HOME') + dpath = home + '/Library/Application Support/Kindle' + cmdline = '/sbin/mount' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + disk = '' + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + if resline.startswith('/dev'): + (devpart, mpath) = resline.split(' on ') + dpart = devpart[5:] + pp = mpath.find('(') + if pp >= 0: + mpath = mpath[:pp-1] + if dpath.startswith(mpath): + disk = dpart + return disk + +# uses a sub process to get the UUID of the specified disk partition using ioreg +def GetDiskPartitionUUID(diskpart): + uuidnum = os.getenv('MYUUIDNUMBER') + if uuidnum != None: + return uuidnum + cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + bsdname = None + uuidnum = None + foundIt = False + nest = 0 + uuidnest = -1 + partnest = -2 + for j in xrange(cnt): + resline = reslst[j] + if resline.find('{') >= 0: + nest += 1 + if resline.find('}') >= 0: + nest -= 1 + pp = resline.find('"UUID" = "') + if pp >= 0: + uuidnum = resline[pp+10:-1] + uuidnum = uuidnum.strip() + uuidnest = nest + if partnest == uuidnest and uuidnest > 0: + foundIt = True + break + bb = resline.find('"BSD Name" = "') + if bb >= 0: + bsdname = resline[bb+14:-1] + bsdname = bsdname.strip() + if (bsdname == diskpart): + partnest = nest + else : + partnest = -2 + if partnest == uuidnest and partnest > 0: + foundIt = True + break + if nest == 0: + partnest = -2 + uuidnest = -1 + uuidnum = None + bsdname = None + if not foundIt: + uuidnum = '' + return uuidnum + +def GetMACAddressMunged(): + macnum = os.getenv('MYMACNUM') + if macnum != None: + return macnum + cmdline = '/sbin/ifconfig en0' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p.communicate() + reslst = out1.split('\n') + cnt = len(reslst) + macnum = None + foundIt = False + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('ether ') + if pp >= 0: + macnum = resline[pp+6:-1] + macnum = macnum.strip() + # print "original mac", macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 + maclst = macnum.split(':') + n = len(maclst) + if n != 6: + fountIt = False + break + for i in range(6): + maclst[i] = int('0x' + maclst[i], 0) + mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + mlst[5] = maclst[5] ^ 0xa5 + mlst[4] = maclst[3] ^ 0xa5 + mlst[3] = maclst[4] ^ 0xa5 + mlst[2] = maclst[2] ^ 0xa5 + mlst[1] = maclst[1] ^ 0xa5 + mlst[0] = maclst[0] ^ 0xa5 + macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) + foundIt = True + break + if not foundIt: + macnum = '' + return macnum + + # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') return username + # implements an Pseudo Mac Version of Windows built-in Crypto routine -def CryptUnprotectData(encryptedData, salt): - sp = GetVolumeSerialNumber() + '!@#' + GetUserName() +# used by Kindle for Mac versions < 1.6.0 +def CryptUnprotectData(encryptedData): + sernum = GetVolumeSerialNumber() + if sernum == '': + sernum = '9999999999' + sp = sernum + '!@#' + GetUserName() passwdData = encode(SHA256(sp),charMap1) + salt = '16743' + iter = 0x3e8 + keylen = 0x80 crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt) + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] crp.set_decrypt_key(key,iv) cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext,charMap1) + return cleartext + + +def isNewInstall(): + home = os.getenv('HOME') + # soccer game fan anyone + dpath = home + '/Library/Application Support/Kindle/storage/.pes2011' + # print dpath, os.path.exists(dpath) + if os.path.exists(dpath): + return True + return False + + +def GetIDString(): + # K4Mac now has an extensive set of ids strings it uses + # in encoding pids and in creating unique passwords + # for use in its own version of CryptUnprotectDataV2 + + # BUT Amazon has now become nasty enough to detect when its app + # is being run under a debugger and actually changes code paths + # including which one of these strings is chosen, all to try + # to prevent reverse engineering + + # Sad really ... they will only hurt their own sales ... + # true book lovers really want to keep their books forever + # and move them to their devices and DRM prevents that so they + # will just buy from someplace else that they can remove + # the DRM from + + # Amazon should know by now that true book lover's are not like + # penniless kids that pirate music, we do not pirate books + + if isNewInstall(): + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + sernum = GetVolumeSerialNumber() + if len(sernum) > 7: + return sernum + diskpart = GetUserHomeAppSupKindleDirParitionName() + uuidnum = GetDiskPartitionUUID(diskpart) + if len(uuidnum) > 7: + return uuidnum + mungedmac = GetMACAddressMunged() + if len(mungedmac) > 7: + return mungedmac + return '9999999999' + + +# implements an Pseudo Mac Version of Windows built-in Crypto routine +# used for Kindle for Mac Versions >= 1.6.0 +def CryptUnprotectDataV2(encryptedData): + sp = GetUserName() + ':&%:' + GetIDString() + passwdData = encode(SHA256(sp),charMap5) + # salt generation as per the code + salt = 0x0512981d * 2 * 1 * 1 + salt = str(salt) + GetUserName() + salt = encode(salt,charMap5) + crp = LibCrypto() + iter = 0x800 + keylen = 0x400 + key_iv = crp.keyivgen(passwdData, salt, iter, keylen) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + cleartext = decode(cleartext, charMap5) return cleartext @@ -232,18 +424,16 @@ def getKindleInfoFiles(kInfoFiles): if os.path.isfile(resline): kInfoFiles.append(resline) found = True - # For Future Reference - # - # # add any .kinf files - # cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"' - # cmdline = cmdline.encode(sys.getfilesystemencoding()) - # p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) - # out1, out2 = p1.communicate() - # reslst = out1.split('\n') - # for resline in reslst: - # if os.path.isfile(resline): - # kInfoFiles.append(resline) - # found = True + # add any .kinf files + cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) + out1, out2 = p1.communicate() + reslst = out1.split('\n') + for resline in reslst: + if os.path.isfile(resline): + kInfoFiles.append(resline) + found = True if not found: print('No kindle-info files have been found.') return kInfoFiles @@ -251,7 +441,7 @@ def getKindleInfoFiles(kInfoFiles): # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') @@ -259,7 +449,6 @@ def getDBfromFile(kInfoFile): data = infoReader.read() if data.find('[') != -1 : - # older style kindle-info file items = data.split('[') for item in items: @@ -273,84 +462,89 @@ def getDBfromFile(kInfoFile): if keyname == "unknown": keyname = keyhash encryptedValue = decode(rawdata,charMap2) - salt = '16743' - cleartext = CryptUnprotectData(encryptedValue, salt) - DB[keyname] = decode(cleartext,charMap1) + cleartext = CryptUnprotectData(encryptedValue) + DB[keyname] = cleartext cnt = cnt + 1 if cnt == 0: DB = None return DB - # For Future Reference taken from K4PC 1.5.0 .kinf - # - # # else newer style .kinf file - # # the .kinf file uses "/" to separate it into records - # # so remove the trailing "/" to make it easy to use split - # data = data[:-1] - # items = data.split('/') - # - # # loop through the item records until all are processed - # while len(items) > 0: - # - # # get the first item record - # item = items.pop(0) - # - # # the first 32 chars of the first record of a group - # # is the MD5 hash of the key name encoded by charMap5 - # keyhash = item[0:32] - # - # # the raw keyhash string is also used to create entropy for the actual - # # CryptProtectData Blob that represents that keys contents - # entropy = SHA1(keyhash) - # - # # the remainder of the first record when decoded with charMap5 - # # has the ':' split char followed by the string representation - # # of the number of records that follow - # # and make up the contents - # srcnt = decode(item[34:],charMap5) - # rcnt = int(srcnt) - # - # # read and store in rcnt records of data - # # that make up the contents value - # edlst = [] - # for i in xrange(rcnt): - # item = items.pop(0) - # edlst.append(item) - # - # keyname = "unknown" - # for name in names: - # if encodeHash(name,charMap5) == keyhash: - # keyname = name - # break - # if keyname == "unknown": - # keyname = keyhash - # - # # the charMap5 encoded contents data has had a length - # # of chars (always odd) cut off of the front and moved - # # to the end to prevent decoding using charMap5 from - # # working properly, and thereby preventing the ensuing - # # CryptUnprotectData call from succeeding. - # - # # The offset into the charMap5 encoded contents seems to be: - # # len(contents) - largest prime number less than or equal to int(len(content)/3) - # # (in other words split "about" 2/3rds of the way through) - # - # # move first offsets chars to end to align for decode by charMap5 - # encdata = "".join(edlst) - # contlen = len(encdata) - # noffset = contlen - primes(int(contlen/3))[-1] - # - # # now properly split and recombine - # # by moving noffset chars from the start of the - # # string to the end of the string - # pfx = encdata[0:noffset] - # encdata = encdata[noffset:] - # encdata = encdata + pfx - # - # # decode using Map5 to get the CryptProtect Data - # encryptedValue = decode(encdata,charMap5) - # DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1) - # cnt = cnt + 1 + # else newer style .kinf file used by K4Mac >= 1.6.0 + # the .kinf file uses "/" to separate it into records + # so remove the trailing "/" to make it easy to use split + data = data[:-1] + items = data.split('/') + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) + + # the first 32 chars of the first record of a group + # is the MD5 hash of the key name encoded by charMap5 + keyhash = item[0:32] + keyname = "unknown" + + # the raw keyhash string is also used to create entropy for the actual + # CryptProtectData Blob that represents that keys contents + # "entropy" not used for K4Mac only K4PC + # entropy = SHA1(keyhash) + + # the remainder of the first record when decoded with charMap5 + # has the ':' split char followed by the string representation + # of the number of records that follow + # and make up the contents + srcnt = decode(item[34:],charMap5) + rcnt = int(srcnt) + + # read and store in rcnt records of data + # that make up the contents value + edlst = [] + for i in xrange(rcnt): + item = items.pop(0) + edlst.append(item) + + keyname = "unknown" + for name in names: + if encodeHash(name,charMap5) == keyhash: + keyname = name + break + if keyname == "unknown": + keyname = keyhash + + # the charMap5 encoded contents data has had a length + # of chars (always odd) cut off of the front and moved + # to the end to prevent decoding using charMap5 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the charMap5 encoded contents seems to be: + # len(contents) - largest prime number less than or equal to int(len(content)/3) + # (in other words split "about" 2/3rds of the way through) + + # move first offsets chars to end to align for decode by charMap5 + encdata = "".join(edlst) + contlen = len(encdata) + + # now properly split and recombine + # by moving noffset chars from the start of the + # string to the end of the string + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + + # decode using charMap5 to get the CryptProtect Data + encryptedValue = decode(encdata,charMap5) + cleartext = CryptUnprotectDataV2(encryptedValue) + # Debugging + # print keyname + # print cleartext + # print cleartext.encode('hex') + # print + DB[keyname] = cleartext + cnt = cnt + 1 if cnt == 0: DB = None diff --git a/KindleBooks/lib/k4pcutils.py b/KindleBooks/lib/k4pcutils.py index 690033b..6acdd5c 100644 --- a/KindleBooks/lib/k4pcutils.py +++ b/KindleBooks/lib/k4pcutils.py @@ -122,6 +122,9 @@ def GetVolumeSerialNumber(): return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() +def GetIDString(): + return GetVolumeSerialNumber() + def getLastError(): GetLastError = kernel32.GetLastError GetLastError.argtypes = None @@ -181,18 +184,27 @@ def getKindleInfoFiles(kInfoFiles): kInfoFiles.append(kinfopath) # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' if not os.path.isfile(kinfopath): - print('No .kinf files have not been found.') + print('No K4PC 1.5.X .kinf files have not been found.') else: kInfoFiles.append(kinfopath) + + # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file + kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' + if not os.path.isfile(kinfopath): + print('No K4PC 1.6.X .kinf files have not been found.') + else: + kInfoFiles.append(kinfopath) + return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): - names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"] DB = {} cnt = 0 infoReader = open(kInfoFile, 'r') diff --git a/KindleBooks/lib/kgenpids.py b/KindleBooks/lib/kgenpids.py index c9d8944..abfc7e4 100644 --- a/KindleBooks/lib/kgenpids.py +++ b/KindleBooks/lib/kgenpids.py @@ -22,16 +22,16 @@ else: if inCalibre: if sys.platform.startswith('win'): - from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString else: if sys.platform.startswith('win'): - from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString if sys.platform.startswith('darwin'): - from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber + from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" @@ -218,14 +218,14 @@ def getK4Pids(pidlst, rec209, token, kInfoFile): print "Keys not found in " + kInfoFile return pidlst - # Get the HDD serial - encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) + # Get the ID string used + encodedIDString = encodeHash(GetIDString(),charMap1) # Get the current user name encodedUsername = encodeHash(GetUserName(),charMap1) # concat, hash and encode to calculate the DSN - DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) # Compute the device PID (for which I can tell, is used for nothing). table = generatePidEncryptionTable() diff --git a/Kindle_for_Android_Patch/ReadMe_K4Android.txt b/Kindle_for_Android_Patch/ReadMe_K4Android.txt index f85e0fd..c4bda17 100644 --- a/Kindle_for_Android_Patch/ReadMe_K4Android.txt +++ b/Kindle_for_Android_Patch/ReadMe_K4Android.txt @@ -25,7 +25,7 @@ adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk adb uninstall com.amazon.kindle apktool d kindle3.apk kindle3 cd kindle3 -patch -p1 < ..\kindle3.patch +patch -p1 < ../kindle3.patch cd .. apktool b kindle3 kindle3_patched.apk jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle