]> xmof Git - DeDRM.git/commitdiff
tools v4.5
authorApprentice Alf <apprenticealf@gmail.com>
Thu, 14 Jul 2011 06:08:06 +0000 (07:08 +0100)
committerApprentice Alf <apprenticealf@gmail.com>
Fri, 6 Mar 2015 07:08:24 +0000 (07:08 +0000)
25 files changed:
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py
Calibre_Plugins/README-Ineptpdf-plugin.txt
Calibre_Plugins/README-K4MobiDeDRM-plugin.txt
Calibre_Plugins/README-eReaderPDB2PML-plugin.txt
Calibre_Plugins/README-ignobleepub-plugin.txt
Calibre_Plugins/README-ineptepub-plugin.txt
Calibre_Plugins/k4mobidedrm_plugin.zip
Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py
Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kgenpids.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py
KindleBooks/lib/k4mobidedrm.py
KindleBooks/lib/k4mutils.py
KindleBooks/lib/k4pcutils.py
KindleBooks/lib/kgenpids.py
Kindle_for_Android_Patch/ReadMe_K4Android.txt

index 971c02b7e162c5a8bdfbd7c85abd6a1e087b8d64..213b2927e13f11fd057f76ad7d5ff2c36aef4bb1 100644 (file)
@@ -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
index 14556db3d25ace71f1ad32bd2ca64d75f7e7a395..2eb5376fe1060d402193d7f83c18bf6676fd7d59 100644 (file)
@@ -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):
index c9d894431d5526dc78fc40cb71e11ef3cd53236e..abfc7e47f6140c907af2fc967d414ef703d3fce4 100644 (file)
@@ -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()
index 457adb11c57dd681493470f3224f30d36e91a4cd..5c4a0266fbf72336d3f13a1d40831505b8ab329b 100644 (file)
@@ -8,7 +8,7 @@ This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected wi
 \r\r
 Installation:\r\r
 \r\r
-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.\r
+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.\r
 \r
 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.\r\r
 \r\r
index 1086890bfbb81c9d2733d04f46660e8b90f534ed..8725cf44e3785453d4c33e7049c8cf23efc2dc2d 100644 (file)
@@ -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.\r\r
 \r\r
 Installation:\r\r
-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.\r
+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.\r
 \r
 \r
 Configuration:\r\r
index ff9831604c960867d290831042b2f02206b67a20..035547edaf52e2310e6d5d6280b8116e3c2d8b1f 100644 (file)
@@ -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.\r\r
 \r\r
 Installation:\r\r
-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.\r
+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.\r
 \r
 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.\r\r
 \r\r
index ad52f5370d0e95c508a57c2d6913ec41fcbf3dac..68aa608ce037d164ca774e0b57ed45c8d37b0e54 100644 (file)
@@ -9,7 +9,7 @@ with Adobe's Adept encryption. It is meant to function without having to install
 \r\r
 Installation:\r\r
 \r\r
-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\r\r
+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\r\r
 click the 'Add' button. you're done.\r\r
 \r
 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.\r\r
index 1bc1d7ab899003ea4b1b6a2a7159171879448b5a..9c58cf06c5f2ec486532f71fc77b03eff96662f8 100644 (file)
@@ -8,7 +8,7 @@ This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected w
 \r\r
 Installation:\r\r
 \r\r
-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.\r
+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.\r
 \r
 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.\r\r
 \r\r
index 00782f0029b3e05b332b73112a3d95cb1cfdcb29..5ca2c278331e1d1fa7650e539ba75b336f5d49c5 100644 (file)
Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ
index 534c3899932fcb1824f648ca01899638fc0b1600..7d5130cbf56a24828c2f542bcbaec1d982e60942 100644 (file)
@@ -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
index 690033b9c1673494d8209c4c2bce49f3fc64ef74..6acdd5c65c6b224a48221d3033a061b1cc570b71 100644 (file)
@@ -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')
index aecb257fe7ebb72e37bb212b160953d97d13814d..4783bb8904e6770eac8c037364566d3a3948d4b9 100644 (file)
@@ -24,7 +24,7 @@
        <key>CFBundleExecutable</key>
        <string>droplet</string>
        <key>CFBundleGetInfoString</key>
-       <string>DeDRM 2.7, Written 2010–2011 by Apprentice Alf and others.</string>
+       <string>DeDRM 2.8, Written 2010–2011 by Apprentice Alf and others.</string>
        <key>CFBundleIconFile</key>
        <string>droplet</string>
        <key>CFBundleInfoDictionaryVersion</key>
@@ -34,7 +34,7 @@
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
-       <string>2.7</string>
+       <string>2.8</string>
        <key>CFBundleSignature</key>
        <string>dplt</string>
        <key>LSMinimumSystemVersion</key>
index 14556db3d25ace71f1ad32bd2ca64d75f7e7a395..2eb5376fe1060d402193d7f83c18bf6676fd7d59 100644 (file)
@@ -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):
index 534c3899932fcb1824f648ca01899638fc0b1600..7d5130cbf56a24828c2f542bcbaec1d982e60942 100644 (file)
@@ -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
index 690033b9c1673494d8209c4c2bce49f3fc64ef74..6acdd5c65c6b224a48221d3033a061b1cc570b71 100644 (file)
@@ -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')
index c9d894431d5526dc78fc40cb71e11ef3cd53236e..abfc7e47f6140c907af2fc967d414ef703d3fce4 100644 (file)
@@ -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()
index 14556db3d25ace71f1ad32bd2ca64d75f7e7a395..2eb5376fe1060d402193d7f83c18bf6676fd7d59 100644 (file)
@@ -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):
index 534c3899932fcb1824f648ca01899638fc0b1600..7d5130cbf56a24828c2f542bcbaec1d982e60942 100644 (file)
@@ -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
index 690033b9c1673494d8209c4c2bce49f3fc64ef74..6acdd5c65c6b224a48221d3033a061b1cc570b71 100644 (file)
@@ -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')
index c9d894431d5526dc78fc40cb71e11ef3cd53236e..abfc7e47f6140c907af2fc967d414ef703d3fce4 100644 (file)
@@ -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()
index 14556db3d25ace71f1ad32bd2ca64d75f7e7a395..2eb5376fe1060d402193d7f83c18bf6676fd7d59 100644 (file)
@@ -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):
index 534c3899932fcb1824f648ca01899638fc0b1600..7d5130cbf56a24828c2f542bcbaec1d982e60942 100644 (file)
@@ -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
index 690033b9c1673494d8209c4c2bce49f3fc64ef74..6acdd5c65c6b224a48221d3033a061b1cc570b71 100644 (file)
@@ -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')
index c9d894431d5526dc78fc40cb71e11ef3cd53236e..abfc7e47f6140c907af2fc967d414ef703d3fce4 100644 (file)
@@ -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()
index f85e0fdf034e78ba4e4531fa6f396355244b4fb9..c4bda171a55dfff958c9898c6ee709994af57215 100644 (file)
@@ -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