From 297a9ddc666fff75e34013314187e73cf2bf54b2 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Thu, 14 Jul 2011 07:08:06 +0100 Subject: [PATCH] tools v4.5 --- .../K4MobiDeDRM_plugin/__init__.py | 2 +- .../K4MobiDeDRM_plugin/k4mobidedrm_orig.py | 2 +- .../K4MobiDeDRM_plugin/kgenpids.py | 14 +- Calibre_Plugins/README-Ineptpdf-plugin.txt | 2 +- Calibre_Plugins/README-K4MobiDeDRM-plugin.txt | 2 +- .../README-eReaderPDB2PML-plugin.txt | 2 +- Calibre_Plugins/README-ignobleepub-plugin.txt | 2 +- Calibre_Plugins/README-ineptepub-plugin.txt | 2 +- Calibre_Plugins/k4mobidedrm_plugin.zip | Bin 47877 -> 49357 bytes .../k4mobidedrm_plugin/k4mutils.py | 396 +++++++++++++----- .../k4mobidedrm_plugin/k4pcutils.py | 16 +- .../DeDRM.app/Contents/Info.plist | 4 +- .../Contents/Resources/k4mobidedrm.py | 2 +- .../DeDRM.app/Contents/Resources/k4mutils.py | 396 +++++++++++++----- .../DeDRM.app/Contents/Resources/k4pcutils.py | 16 +- .../DeDRM.app/Contents/Resources/kgenpids.py | 14 +- .../DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py | 2 +- .../DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py | 396 +++++++++++++----- .../DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py | 16 +- .../DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py | 14 +- KindleBooks/lib/k4mobidedrm.py | 2 +- KindleBooks/lib/k4mutils.py | 396 +++++++++++++----- KindleBooks/lib/k4pcutils.py | 16 +- KindleBooks/lib/kgenpids.py | 14 +- Kindle_for_Android_Patch/ReadMe_K4Android.txt | 2 +- 25 files changed, 1277 insertions(+), 453 deletions(-) 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 00782f0029b3e05b332b73112a3d95cb1cfdcb29..5ca2c278331e1d1fa7650e539ba75b336f5d49c5 100644 GIT binary patch delta 15985 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 BN`3$U 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