diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py index ddba7a42..7908e6bf 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py @@ -48,6 +48,9 @@ # 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 # 6.4.0 - Updated for new Kindle for PC encryption # 6.4.1 - Fix for some new tags in Topaz ebooks. +# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks +# 6.4.3 - Fix for error that only appears when not in debug mode +# Also includes fix for Macs with bonded ethernet ports """ @@ -55,7 +58,7 @@ """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 4, 1) +PLUGIN_VERSION_TUPLE = (6, 4, 3) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -87,8 +90,12 @@ def __init__(self, stream): def write(self, data): if isinstance(data,unicode): data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() + try: + self.stream.write(data) + self.stream.flush() + except: + # We can do nothing if a write fails + pass def __getattr__(self, attr): return getattr(self.stream, attr) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py index 79b17f28..3a56e44f 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py @@ -566,6 +566,19 @@ def __init__(self, parent=None,): data_group_box_layout.addWidget(ccn_disclaimer_label) layout.addSpacing(10) + key_group = QHBoxLayout() + data_group_box_layout.addLayout(key_group) + key_group.addWidget(QLabel(u"Retrieved key:", self)) + self.key_display = QLabel(u"", self) + self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) + key_group.addWidget(self.key_display) + self.retrieve_button = QtGui.QPushButton(self) + self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers")) + self.retrieve_button.setText(u"Retrieve Key") + self.retrieve_button.clicked.connect(self.retrieve_key) + key_group.addWidget(self.retrieve_button) + layout.addSpacing(10) + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) @@ -579,8 +592,7 @@ def key_name(self): @property def key_value(self): - from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key - return fetch_bandn_key(self.user_name,self.cc_number) + return unicode(self.key_display.text()).strip() @property def user_name(self): @@ -590,6 +602,14 @@ def user_name(self): def cc_number(self): return unicode(self.cc_ledit.text()).strip() + def retrieve_key(self): + from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key + fetched_key = fetch_bandn_key(self.user_name,self.cc_number) + if fetched_key == "": + errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again." + error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + else: + self.key_display.setText(fetched_key) def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): @@ -598,6 +618,10 @@ def accept(self): if len(self.key_name) < 4: errmsg = u"Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + if len(self.key_value) == 0: + self.retrieve_key() + if len(self.key_value) == 0: + return QDialog.accept(self) class AddEReaderDialog(QDialog): diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py index c5159ccb..493f950c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/kindlekey.py @@ -4,7 +4,7 @@ from __future__ import with_statement # kindlekey.py -# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper +# Copyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. @@ -19,6 +19,9 @@ # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names # 1.9 - Fixes for Unicode in Windows user names # 2.0 - Added comments and extra fix for non-ascii Windows user names +# 2.1 - Fixed Kindle for PC encryption changes March 2016 +# 2.2 - Fixes for Macs with bonded ethernet ports +# Also removed old .kinfo file support (pre-2011) """ @@ -26,7 +29,7 @@ """ __license__ = 'GPL v3' -__version__ = '1.9' +__version__ = '2.2' import sys, os, re from struct import pack, unpack, unpack_from @@ -926,7 +929,7 @@ def getKindleInfoFiles(): # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x + # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") # this is just another alternative. # path = getEnvironmentVariable('LOCALAPPDATA') @@ -994,192 +997,113 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] DB = {} with open(kInfoFile, 'rb') as infoReader: - hdr = infoReader.read(1) data = infoReader.read() - - if data.find('{') != -1 : - # older style kindle-info file - items = data.split('{') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) - elif hdr == '/': - # else rainier-2-1-1 .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 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 <= 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) - else: - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record + # assume newest .kinf2011 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('/') + + # starts with an encoded and encrypted header blob + headerblob = items.pop(0) + encryptedValue = decode(headerblob, testMap1) + cleartext = UnprotectHeaderData(encryptedValue) + #print "header cleartext:",cleartext + # now extract the pieces that form the added entropy + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + added_entropy = m.group(2) + m.group(4) + + + # 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 sha1 of raw keyhash string is used to create entropy along + # with the added entropy provided above from the headerblob + entropy = SHA1(keyhash) + added_entropy + + # 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) - - # 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 sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # 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) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= 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 testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>4: + edlst.append(item) + + # key names now use the new testMap8 encoding + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + #print "keyname found from hash:",keyname + break + if keyname == "unknown": + keyname = keyhash + #print "keyname not found, hash is:",keyname + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents)-largest prime number <= 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 testMap8 + # by moving noffset chars from the start of the + # string to the end of the string + encdata = "".join(edlst) + #print "encrypted data:",encdata + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + #print "rearranged data:",encdata + + + # decode using new testMap8 to get the original CryptProtect Data + encryptedValue = decode(encdata,testMap8) + #print "decoded data:",encryptedValue.encode('hex') + cleartext = CryptUnprotectData(encryptedValue, entropy, 1) + if len(cleartext)>0: + #print "cleartext data:",cleartext,":end data" + DB[keyname] = cleartext + #print keyname, cleartext + + if len(DB)>6: # store values used in decryption DB['IDString'] = GetIDString() DB['UserName'] = GetUserName() @@ -1317,11 +1241,9 @@ def GetVolumesSerialNumbers(): 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() + #print out1 reslst = out1.split('\n') cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False for j in xrange(cnt): resline = reslst[j] pp = resline.find('\"Serial Number\" = \"') @@ -1330,31 +1252,24 @@ def GetVolumesSerialNumbers(): sernums.append(sernum.strip()) return sernums - def GetUserHomeAppSupKindleDirParitionName(): - home = os.getenv('HOME') - dpath = home + '/Library' + def GetDiskPartitionNames(): + names = [] 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 GetDiskPartitionUUIDs(diskpart): + names.append(dpart) + return names + + # uses a sub process to get the UUID of all disk partitions + def GetDiskPartitionUUIDs(): uuids = [] uuidnum = os.getenv('MYUUIDNUMBER') if uuidnum != None: @@ -1363,46 +1278,16 @@ def GetDiskPartitionUUIDs(diskpart): 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() + #print out1 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 foundIt: - uuids.append(uuidnum) + uuids.append(uuidnum) return uuids def GetMACAddressesMunged(): @@ -1410,28 +1295,26 @@ def GetMACAddressesMunged(): macnum = os.getenv('MYMACNUM') if macnum != None: macnums.append(macnum) - cmdline = '/sbin/ifconfig en0' + cmdline = 'networksetup -listallhardwareports' # 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 ') + pp = resline.find('Ethernet Address: ') if pp >= 0: - macnum = resline[pp+6:-1] + #print resline + macnum = resline[pp+18:] 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 + continue + #print 'original mac', macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 for i in range(6): maclst[i] = int('0x' + maclst[i], 0) mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] @@ -1442,16 +1325,15 @@ def GetMACAddressesMunged(): 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 foundIt: - macnums.append(macnum) + #print 'munged mac', macnum + macnums.append(macnum) return macnums # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') + #print "Username:",username return username def GetIDStrings(): @@ -1459,58 +1341,13 @@ def GetIDStrings(): strings = [] strings.extend(GetMACAddressesMunged()) strings.extend(GetVolumesSerialNumbers()) - diskpart = GetUserHomeAppSupKindleDirParitionName() - strings.extend(GetDiskPartitionUUIDs(diskpart)) + strings.extend(GetDiskPartitionNames()) + strings.extend(GetDiskPartitionUUIDs()) strings.append('9999999999') - #print strings + #print "ID Strings:\n",strings return strings - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used by Kindle for Mac versions < 1.6.0 - class CryptUnprotectData(object): - def __init__(self, IDString): - sp = IDString + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.6.0 - class CryptUnprotectDataV2(object): - def __init__(self, IDString): - sp = GetUserName() + ':&%:' + IDString - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - # unprotect the new header blob in .kinf2011 # used in Kindle for Mac Version >= 1.9.0 def UnprotectHeaderData(encryptedData): @@ -1528,8 +1365,7 @@ def UnprotectHeaderData(encryptedData): # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.9.0 - class CryptUnprotectDataV3(object): + class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + '+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) @@ -1598,219 +1434,117 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] with open(kInfoFile, 'rb') as infoReader: - filehdr = infoReader.read(1) filedata = infoReader.read() + data = filedata[:-1] + items = data.split('/') IDStrings = GetIDStrings() for IDString in IDStrings: - DB = {} #print "trying IDString:",IDString try: - hdr = filehdr - data = filedata - if data.find('[') != -1 : - # older style kindle-info file - cud = CryptUnprotectData(IDString) - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - elif hdr == '/': - # 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('/') - cud = CryptUnprotectDataV2(IDString) - - # 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 = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break - else: - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy,IDString) - - # loop through the item records until all are processed - while len(items) > 0: + DB = {} + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectData(entropy,IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) - # get the first item record + # 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' + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # 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) - # 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' - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # 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,testMap8) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 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 testMap8 - 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 testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 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 testMap8 + 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 testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + if len(cleartext) > 0: + DB[keyname] = cleartext + + if len(DB)>6: + break except: pass - if len(DB)>4: + if len(DB)>6: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString @@ -1874,7 +1608,7 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) + print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) try: opts, args = getopt.getopt(argv[1:], "hk:") @@ -1904,7 +1638,7 @@ def cli_main(): # save to the same directory as the script outpath = os.path.dirname(argv[0]) - # make sure the outpath is the + # make sure the outpath is canonical outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_App.pyw b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_App.pyw index 6ce14117..3221ecf8 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_App.pyw +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_App.pyw @@ -24,8 +24,9 @@ # 6.4.0 - Fix for Kindle for PC encryption change # 6.4.1 - Fix for new tags in Topaz ebooks # 6.4.2 - Fix for new tags in Topaz ebooks, and very small Topaz ebooks +# 6.4.3 - Version bump to match plugin & Mac app -__version__ = '6.4.2' +__version__ = '6.4.3' import sys import os, os.path diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py index ddba7a42..7908e6bf 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py @@ -48,6 +48,9 @@ # 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 # 6.4.0 - Updated for new Kindle for PC encryption # 6.4.1 - Fix for some new tags in Topaz ebooks. +# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks +# 6.4.3 - Fix for error that only appears when not in debug mode +# Also includes fix for Macs with bonded ethernet ports """ @@ -55,7 +58,7 @@ """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 4, 1) +PLUGIN_VERSION_TUPLE = (6, 4, 3) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -87,8 +90,12 @@ def __init__(self, stream): def write(self, data): if isinstance(data,unicode): data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() + try: + self.stream.write(data) + self.stream.flush() + except: + # We can do nothing if a write fails + pass def __getattr__(self, attr): return getattr(self.stream, attr) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py index 79b17f28..3a56e44f 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py @@ -566,6 +566,19 @@ def __init__(self, parent=None,): data_group_box_layout.addWidget(ccn_disclaimer_label) layout.addSpacing(10) + key_group = QHBoxLayout() + data_group_box_layout.addLayout(key_group) + key_group.addWidget(QLabel(u"Retrieved key:", self)) + self.key_display = QLabel(u"", self) + self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) + key_group.addWidget(self.key_display) + self.retrieve_button = QtGui.QPushButton(self) + self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers")) + self.retrieve_button.setText(u"Retrieve Key") + self.retrieve_button.clicked.connect(self.retrieve_key) + key_group.addWidget(self.retrieve_button) + layout.addSpacing(10) + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) @@ -579,8 +592,7 @@ def key_name(self): @property def key_value(self): - from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key - return fetch_bandn_key(self.user_name,self.cc_number) + return unicode(self.key_display.text()).strip() @property def user_name(self): @@ -590,6 +602,14 @@ def user_name(self): def cc_number(self): return unicode(self.cc_ledit.text()).strip() + def retrieve_key(self): + from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key + fetched_key = fetch_bandn_key(self.user_name,self.cc_number) + if fetched_key == "": + errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again." + error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + else: + self.key_display.setText(fetched_key) def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): @@ -598,6 +618,10 @@ def accept(self): if len(self.key_name) < 4: errmsg = u"Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + if len(self.key_value) == 0: + self.retrieve_key() + if len(self.key_value) == 0: + return QDialog.accept(self) class AddEReaderDialog(QDialog): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py index c5159ccb..493f950c 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/kindlekey.py @@ -4,7 +4,7 @@ from __future__ import with_statement # kindlekey.py -# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper +# Copyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. @@ -19,6 +19,9 @@ # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names # 1.9 - Fixes for Unicode in Windows user names # 2.0 - Added comments and extra fix for non-ascii Windows user names +# 2.1 - Fixed Kindle for PC encryption changes March 2016 +# 2.2 - Fixes for Macs with bonded ethernet ports +# Also removed old .kinfo file support (pre-2011) """ @@ -26,7 +29,7 @@ """ __license__ = 'GPL v3' -__version__ = '1.9' +__version__ = '2.2' import sys, os, re from struct import pack, unpack, unpack_from @@ -926,7 +929,7 @@ def getKindleInfoFiles(): # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x + # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") # this is just another alternative. # path = getEnvironmentVariable('LOCALAPPDATA') @@ -994,192 +997,113 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] DB = {} with open(kInfoFile, 'rb') as infoReader: - hdr = infoReader.read(1) data = infoReader.read() - - if data.find('{') != -1 : - # older style kindle-info file - items = data.split('{') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) - elif hdr == '/': - # else rainier-2-1-1 .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 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 <= 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) - else: - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record + # assume newest .kinf2011 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('/') + + # starts with an encoded and encrypted header blob + headerblob = items.pop(0) + encryptedValue = decode(headerblob, testMap1) + cleartext = UnprotectHeaderData(encryptedValue) + #print "header cleartext:",cleartext + # now extract the pieces that form the added entropy + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + added_entropy = m.group(2) + m.group(4) + + + # 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 sha1 of raw keyhash string is used to create entropy along + # with the added entropy provided above from the headerblob + entropy = SHA1(keyhash) + added_entropy + + # 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) - - # 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 sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # 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) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= 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 testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>4: + edlst.append(item) + + # key names now use the new testMap8 encoding + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + #print "keyname found from hash:",keyname + break + if keyname == "unknown": + keyname = keyhash + #print "keyname not found, hash is:",keyname + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents)-largest prime number <= 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 testMap8 + # by moving noffset chars from the start of the + # string to the end of the string + encdata = "".join(edlst) + #print "encrypted data:",encdata + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + #print "rearranged data:",encdata + + + # decode using new testMap8 to get the original CryptProtect Data + encryptedValue = decode(encdata,testMap8) + #print "decoded data:",encryptedValue.encode('hex') + cleartext = CryptUnprotectData(encryptedValue, entropy, 1) + if len(cleartext)>0: + #print "cleartext data:",cleartext,":end data" + DB[keyname] = cleartext + #print keyname, cleartext + + if len(DB)>6: # store values used in decryption DB['IDString'] = GetIDString() DB['UserName'] = GetUserName() @@ -1317,11 +1241,9 @@ def GetVolumesSerialNumbers(): 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() + #print out1 reslst = out1.split('\n') cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False for j in xrange(cnt): resline = reslst[j] pp = resline.find('\"Serial Number\" = \"') @@ -1330,31 +1252,24 @@ def GetVolumesSerialNumbers(): sernums.append(sernum.strip()) return sernums - def GetUserHomeAppSupKindleDirParitionName(): - home = os.getenv('HOME') - dpath = home + '/Library' + def GetDiskPartitionNames(): + names = [] 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 GetDiskPartitionUUIDs(diskpart): + names.append(dpart) + return names + + # uses a sub process to get the UUID of all disk partitions + def GetDiskPartitionUUIDs(): uuids = [] uuidnum = os.getenv('MYUUIDNUMBER') if uuidnum != None: @@ -1363,46 +1278,16 @@ def GetDiskPartitionUUIDs(diskpart): 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() + #print out1 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 foundIt: - uuids.append(uuidnum) + uuids.append(uuidnum) return uuids def GetMACAddressesMunged(): @@ -1410,28 +1295,26 @@ def GetMACAddressesMunged(): macnum = os.getenv('MYMACNUM') if macnum != None: macnums.append(macnum) - cmdline = '/sbin/ifconfig en0' + cmdline = 'networksetup -listallhardwareports' # 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 ') + pp = resline.find('Ethernet Address: ') if pp >= 0: - macnum = resline[pp+6:-1] + #print resline + macnum = resline[pp+18:] 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 + continue + #print 'original mac', macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 for i in range(6): maclst[i] = int('0x' + maclst[i], 0) mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] @@ -1442,16 +1325,15 @@ def GetMACAddressesMunged(): 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 foundIt: - macnums.append(macnum) + #print 'munged mac', macnum + macnums.append(macnum) return macnums # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') + #print "Username:",username return username def GetIDStrings(): @@ -1459,58 +1341,13 @@ def GetIDStrings(): strings = [] strings.extend(GetMACAddressesMunged()) strings.extend(GetVolumesSerialNumbers()) - diskpart = GetUserHomeAppSupKindleDirParitionName() - strings.extend(GetDiskPartitionUUIDs(diskpart)) + strings.extend(GetDiskPartitionNames()) + strings.extend(GetDiskPartitionUUIDs()) strings.append('9999999999') - #print strings + #print "ID Strings:\n",strings return strings - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used by Kindle for Mac versions < 1.6.0 - class CryptUnprotectData(object): - def __init__(self, IDString): - sp = IDString + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.6.0 - class CryptUnprotectDataV2(object): - def __init__(self, IDString): - sp = GetUserName() + ':&%:' + IDString - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - # unprotect the new header blob in .kinf2011 # used in Kindle for Mac Version >= 1.9.0 def UnprotectHeaderData(encryptedData): @@ -1528,8 +1365,7 @@ def UnprotectHeaderData(encryptedData): # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.9.0 - class CryptUnprotectDataV3(object): + class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + '+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) @@ -1598,219 +1434,117 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] with open(kInfoFile, 'rb') as infoReader: - filehdr = infoReader.read(1) filedata = infoReader.read() + data = filedata[:-1] + items = data.split('/') IDStrings = GetIDStrings() for IDString in IDStrings: - DB = {} #print "trying IDString:",IDString try: - hdr = filehdr - data = filedata - if data.find('[') != -1 : - # older style kindle-info file - cud = CryptUnprotectData(IDString) - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - elif hdr == '/': - # 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('/') - cud = CryptUnprotectDataV2(IDString) - - # 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 = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break - else: - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy,IDString) - - # loop through the item records until all are processed - while len(items) > 0: + DB = {} + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectData(entropy,IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) - # get the first item record + # 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' + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # 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) - # 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' - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # 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,testMap8) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 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 testMap8 - 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 testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 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 testMap8 + 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 testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + if len(cleartext) > 0: + DB[keyname] = cleartext + + if len(DB)>6: + break except: pass - if len(DB)>4: + if len(DB)>6: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString @@ -1874,7 +1608,7 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) + print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) try: opts, args = getopt.getopt(argv[1:], "hk:") @@ -1904,7 +1638,7 @@ def cli_main(): # save to the same directory as the script outpath = os.path.dirname(argv[0]) - # make sure the outpath is the + # make sure the outpath is canonical outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files): diff --git a/DeDRM_calibre_plugin/DeDRM_plugin.zip b/DeDRM_calibre_plugin/DeDRM_plugin.zip index ef69a25b..ef70c60e 100644 Binary files a/DeDRM_calibre_plugin/DeDRM_plugin.zip and b/DeDRM_calibre_plugin/DeDRM_plugin.zip differ diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py index 080c1947..7908e6bf 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py @@ -49,6 +49,8 @@ # 6.4.0 - Updated for new Kindle for PC encryption # 6.4.1 - Fix for some new tags in Topaz ebooks. # 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks +# 6.4.3 - Fix for error that only appears when not in debug mode +# Also includes fix for Macs with bonded ethernet ports """ @@ -56,7 +58,7 @@ """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 4, 2) +PLUGIN_VERSION_TUPLE = (6, 4, 3) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -88,8 +90,12 @@ def __init__(self, stream): def write(self, data): if isinstance(data,unicode): data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() + try: + self.stream.write(data) + self.stream.flush() + except: + # We can do nothing if a write fails + pass def __getattr__(self, attr): return getattr(self.stream, attr) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/config.py b/DeDRM_calibre_plugin/DeDRM_plugin/config.py index 79b17f28..3a56e44f 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/config.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/config.py @@ -566,6 +566,19 @@ def __init__(self, parent=None,): data_group_box_layout.addWidget(ccn_disclaimer_label) layout.addSpacing(10) + key_group = QHBoxLayout() + data_group_box_layout.addLayout(key_group) + key_group.addWidget(QLabel(u"Retrieved key:", self)) + self.key_display = QLabel(u"", self) + self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) + key_group.addWidget(self.key_display) + self.retrieve_button = QtGui.QPushButton(self) + self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers")) + self.retrieve_button.setText(u"Retrieve Key") + self.retrieve_button.clicked.connect(self.retrieve_key) + key_group.addWidget(self.retrieve_button) + layout.addSpacing(10) + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) @@ -579,8 +592,7 @@ def key_name(self): @property def key_value(self): - from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key - return fetch_bandn_key(self.user_name,self.cc_number) + return unicode(self.key_display.text()).strip() @property def user_name(self): @@ -590,6 +602,14 @@ def user_name(self): def cc_number(self): return unicode(self.cc_ledit.text()).strip() + def retrieve_key(self): + from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key + fetched_key = fetch_bandn_key(self.user_name,self.cc_number) + if fetched_key == "": + errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again." + error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + else: + self.key_display.setText(fetched_key) def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): @@ -598,6 +618,10 @@ def accept(self): if len(self.key_name) < 4: errmsg = u"Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) + if len(self.key_value) == 0: + self.retrieve_key() + if len(self.key_value) == 0: + return QDialog.accept(self) class AddEReaderDialog(QDialog): diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py b/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py index c5159ccb..493f950c 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/kindlekey.py @@ -4,7 +4,7 @@ from __future__ import with_statement # kindlekey.py -# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper +# Copyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. @@ -19,6 +19,9 @@ # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names # 1.9 - Fixes for Unicode in Windows user names # 2.0 - Added comments and extra fix for non-ascii Windows user names +# 2.1 - Fixed Kindle for PC encryption changes March 2016 +# 2.2 - Fixes for Macs with bonded ethernet ports +# Also removed old .kinfo file support (pre-2011) """ @@ -26,7 +29,7 @@ """ __license__ = 'GPL v3' -__version__ = '1.9' +__version__ = '2.2' import sys, os, re from struct import pack, unpack, unpack_from @@ -926,7 +929,7 @@ def getKindleInfoFiles(): # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x + # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") # this is just another alternative. # path = getEnvironmentVariable('LOCALAPPDATA') @@ -994,192 +997,113 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] DB = {} with open(kInfoFile, 'rb') as infoReader: - hdr = infoReader.read(1) data = infoReader.read() - - if data.find('{') != -1 : - # older style kindle-info file - items = data.split('{') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) - elif hdr == '/': - # else rainier-2-1-1 .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 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 <= 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) - else: - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record + # assume newest .kinf2011 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('/') + + # starts with an encoded and encrypted header blob + headerblob = items.pop(0) + encryptedValue = decode(headerblob, testMap1) + cleartext = UnprotectHeaderData(encryptedValue) + #print "header cleartext:",cleartext + # now extract the pieces that form the added entropy + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + added_entropy = m.group(2) + m.group(4) + + + # 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 sha1 of raw keyhash string is used to create entropy along + # with the added entropy provided above from the headerblob + entropy = SHA1(keyhash) + added_entropy + + # 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) - - # 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 sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # 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) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= 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 testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>4: + edlst.append(item) + + # key names now use the new testMap8 encoding + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + #print "keyname found from hash:",keyname + break + if keyname == "unknown": + keyname = keyhash + #print "keyname not found, hash is:",keyname + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents)-largest prime number <= 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 testMap8 + # by moving noffset chars from the start of the + # string to the end of the string + encdata = "".join(edlst) + #print "encrypted data:",encdata + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + #print "rearranged data:",encdata + + + # decode using new testMap8 to get the original CryptProtect Data + encryptedValue = decode(encdata,testMap8) + #print "decoded data:",encryptedValue.encode('hex') + cleartext = CryptUnprotectData(encryptedValue, entropy, 1) + if len(cleartext)>0: + #print "cleartext data:",cleartext,":end data" + DB[keyname] = cleartext + #print keyname, cleartext + + if len(DB)>6: # store values used in decryption DB['IDString'] = GetIDString() DB['UserName'] = GetUserName() @@ -1317,11 +1241,9 @@ def GetVolumesSerialNumbers(): 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() + #print out1 reslst = out1.split('\n') cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False for j in xrange(cnt): resline = reslst[j] pp = resline.find('\"Serial Number\" = \"') @@ -1330,31 +1252,24 @@ def GetVolumesSerialNumbers(): sernums.append(sernum.strip()) return sernums - def GetUserHomeAppSupKindleDirParitionName(): - home = os.getenv('HOME') - dpath = home + '/Library' + def GetDiskPartitionNames(): + names = [] 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 GetDiskPartitionUUIDs(diskpart): + names.append(dpart) + return names + + # uses a sub process to get the UUID of all disk partitions + def GetDiskPartitionUUIDs(): uuids = [] uuidnum = os.getenv('MYUUIDNUMBER') if uuidnum != None: @@ -1363,46 +1278,16 @@ def GetDiskPartitionUUIDs(diskpart): 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() + #print out1 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 foundIt: - uuids.append(uuidnum) + uuids.append(uuidnum) return uuids def GetMACAddressesMunged(): @@ -1410,28 +1295,26 @@ def GetMACAddressesMunged(): macnum = os.getenv('MYMACNUM') if macnum != None: macnums.append(macnum) - cmdline = '/sbin/ifconfig en0' + cmdline = 'networksetup -listallhardwareports' # 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 ') + pp = resline.find('Ethernet Address: ') if pp >= 0: - macnum = resline[pp+6:-1] + #print resline + macnum = resline[pp+18:] 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 + continue + #print 'original mac', macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 for i in range(6): maclst[i] = int('0x' + maclst[i], 0) mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] @@ -1442,16 +1325,15 @@ def GetMACAddressesMunged(): 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 foundIt: - macnums.append(macnum) + #print 'munged mac', macnum + macnums.append(macnum) return macnums # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') + #print "Username:",username return username def GetIDStrings(): @@ -1459,58 +1341,13 @@ def GetIDStrings(): strings = [] strings.extend(GetMACAddressesMunged()) strings.extend(GetVolumesSerialNumbers()) - diskpart = GetUserHomeAppSupKindleDirParitionName() - strings.extend(GetDiskPartitionUUIDs(diskpart)) + strings.extend(GetDiskPartitionNames()) + strings.extend(GetDiskPartitionUUIDs()) strings.append('9999999999') - #print strings + #print "ID Strings:\n",strings return strings - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used by Kindle for Mac versions < 1.6.0 - class CryptUnprotectData(object): - def __init__(self, IDString): - sp = IDString + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.6.0 - class CryptUnprotectDataV2(object): - def __init__(self, IDString): - sp = GetUserName() + ':&%:' + IDString - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - # unprotect the new header blob in .kinf2011 # used in Kindle for Mac Version >= 1.9.0 def UnprotectHeaderData(encryptedData): @@ -1528,8 +1365,7 @@ def UnprotectHeaderData(encryptedData): # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.9.0 - class CryptUnprotectDataV3(object): + class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + '+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) @@ -1598,219 +1434,117 @@ def getKindleInfoFiles(): # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] with open(kInfoFile, 'rb') as infoReader: - filehdr = infoReader.read(1) filedata = infoReader.read() + data = filedata[:-1] + items = data.split('/') IDStrings = GetIDStrings() for IDString in IDStrings: - DB = {} #print "trying IDString:",IDString try: - hdr = filehdr - data = filedata - if data.find('[') != -1 : - # older style kindle-info file - cud = CryptUnprotectData(IDString) - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - elif hdr == '/': - # 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('/') - cud = CryptUnprotectDataV2(IDString) - - # 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 = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break - else: - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy,IDString) - - # loop through the item records until all are processed - while len(items) > 0: + DB = {} + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectData(entropy,IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) - # get the first item record + # 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' + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # 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) - # 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' - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # 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,testMap8) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 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 testMap8 - 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 testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 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 testMap8 + 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 testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + if len(cleartext) > 0: + DB[keyname] = cleartext + + if len(DB)>6: + break except: pass - if len(DB)>4: + if len(DB)>6: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString @@ -1874,7 +1608,7 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) + print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) try: opts, args = getopt.getopt(argv[1:], "hk:") @@ -1904,7 +1638,7 @@ def cli_main(): # save to the same directory as the script outpath = os.path.dirname(argv[0]) - # make sure the outpath is the + # make sure the outpath is canonical outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files): diff --git a/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw b/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw index c5159ccb..493f950c 100644 --- a/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw +++ b/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw @@ -4,7 +4,7 @@ from __future__ import with_statement # kindlekey.py -# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper +# Copyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. @@ -19,6 +19,9 @@ from __future__ import with_statement # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names # 1.9 - Fixes for Unicode in Windows user names # 2.0 - Added comments and extra fix for non-ascii Windows user names +# 2.1 - Fixed Kindle for PC encryption changes March 2016 +# 2.2 - Fixes for Macs with bonded ethernet ports +# Also removed old .kinfo file support (pre-2011) """ @@ -26,7 +29,7 @@ Retrieve Kindle for PC/Mac user key. """ __license__ = 'GPL v3' -__version__ = '1.9' +__version__ = '2.2' import sys, os, re from struct import pack, unpack, unpack_from @@ -926,7 +929,7 @@ if iswindows: # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): - # Python 2.x does not return unicode env. Use Python 3.x + # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") # this is just another alternative. # path = getEnvironmentVariable('LOCALAPPDATA') @@ -994,192 +997,113 @@ if iswindows: # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] DB = {} with open(kInfoFile, 'rb') as infoReader: - hdr = infoReader.read(1) data = infoReader.read() - - if data.find('{') != -1 : - # older style kindle-info file - items = data.split('{') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = "unknown" - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - DB[keyname] = CryptUnprotectData(encryptedValue, "", 0) - elif hdr == '/': - # else rainier-2-1-1 .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 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 <= 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) - else: - # else newest .kinf2011 style .kinf file - # the .kinf file uses "/" to separate it into records - # so remove the trailing "/" to make it easy to use split - # need to put back the first char read because it it part - # of the added entropy blob - data = hdr + data[:-1] - items = data.split('/') - - # starts with and encoded and encrypted header blob - headerblob = items.pop(0) - encryptedValue = decode(headerblob, testMap1) - cleartext = UnprotectHeaderData(encryptedValue) - # now extract the pieces that form the added entropy - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - added_entropy = m.group(2) + m.group(4) - - - # loop through the item records until all are processed - while len(items) > 0: - - # get the first item record + # assume newest .kinf2011 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('/') + + # starts with an encoded and encrypted header blob + headerblob = items.pop(0) + encryptedValue = decode(headerblob, testMap1) + cleartext = UnprotectHeaderData(encryptedValue) + #print "header cleartext:",cleartext + # now extract the pieces that form the added entropy + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + added_entropy = m.group(2) + m.group(4) + + + # 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 sha1 of raw keyhash string is used to create entropy along + # with the added entropy provided above from the headerblob + entropy = SHA1(keyhash) + added_entropy + + # 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) - - # 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 sha1 of raw keyhash string is used to create entropy along - # with the added entropy provided above from the headerblob - entropy = SHA1(keyhash) + added_entropy - - # 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) - - # key names now use the new testMap8 encoding - keyname = "unknown" - for name in names: - if encodeHash(name,testMap8) == keyhash: - keyname = name - break - if keyname == "unknown": - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 encoded contents seems to be: - # len(contents)-largest prime number <= 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 testMap8 - # by moving noffset chars from the start of the - # string to the end of the string - encdata = "".join(edlst) - contlen = len(encdata) - noffset = contlen - primes(int(contlen/3))[-1] - pfx = encdata[0:noffset] - encdata = encdata[noffset:] - encdata = encdata + pfx - - # decode using new testMap8 to get the original CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = CryptUnprotectData(encryptedValue, entropy, 1) - if len(cleartext)>0: - DB[keyname] = cleartext - #print keyname, cleartext - - if len(DB)>4: + edlst.append(item) + + # key names now use the new testMap8 encoding + keyname = "unknown" + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + #print "keyname found from hash:",keyname + break + if keyname == "unknown": + keyname = keyhash + #print "keyname not found, hash is:",keyname + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 encoded contents seems to be: + # len(contents)-largest prime number <= 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 testMap8 + # by moving noffset chars from the start of the + # string to the end of the string + encdata = "".join(edlst) + #print "encrypted data:",encdata + contlen = len(encdata) + noffset = contlen - primes(int(contlen/3))[-1] + pfx = encdata[0:noffset] + encdata = encdata[noffset:] + encdata = encdata + pfx + #print "rearranged data:",encdata + + + # decode using new testMap8 to get the original CryptProtect Data + encryptedValue = decode(encdata,testMap8) + #print "decoded data:",encryptedValue.encode('hex') + cleartext = CryptUnprotectData(encryptedValue, entropy, 1) + if len(cleartext)>0: + #print "cleartext data:",cleartext,":end data" + DB[keyname] = cleartext + #print keyname, cleartext + + if len(DB)>6: # store values used in decryption DB['IDString'] = GetIDString() DB['UserName'] = GetUserName() @@ -1317,11 +1241,9 @@ elif isosx: 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() + #print out1 reslst = out1.split('\n') cnt = len(reslst) - bsdname = None - sernum = None - foundIt = False for j in xrange(cnt): resline = reslst[j] pp = resline.find('\"Serial Number\" = \"') @@ -1330,31 +1252,24 @@ elif isosx: sernums.append(sernum.strip()) return sernums - def GetUserHomeAppSupKindleDirParitionName(): - home = os.getenv('HOME') - dpath = home + '/Library' + def GetDiskPartitionNames(): + names = [] 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 GetDiskPartitionUUIDs(diskpart): + names.append(dpart) + return names + + # uses a sub process to get the UUID of all disk partitions + def GetDiskPartitionUUIDs(): uuids = [] uuidnum = os.getenv('MYUUIDNUMBER') if uuidnum != None: @@ -1363,46 +1278,16 @@ elif isosx: 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() + #print out1 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 foundIt: - uuids.append(uuidnum) + uuids.append(uuidnum) return uuids def GetMACAddressesMunged(): @@ -1410,28 +1295,26 @@ elif isosx: macnum = os.getenv('MYMACNUM') if macnum != None: macnums.append(macnum) - cmdline = '/sbin/ifconfig en0' + cmdline = 'networksetup -listallhardwareports' # 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 ') + pp = resline.find('Ethernet Address: ') if pp >= 0: - macnum = resline[pp+6:-1] + #print resline + macnum = resline[pp+18:] 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 + continue + #print 'original mac', macnum + # now munge it up the way Kindle app does + # by xoring it with 0xa5 and swapping elements 3 and 4 for i in range(6): maclst[i] = int('0x' + maclst[i], 0) mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] @@ -1442,16 +1325,15 @@ elif isosx: 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 foundIt: - macnums.append(macnum) + #print 'munged mac', macnum + macnums.append(macnum) return macnums # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') + #print "Username:",username return username def GetIDStrings(): @@ -1459,58 +1341,13 @@ elif isosx: strings = [] strings.extend(GetMACAddressesMunged()) strings.extend(GetVolumesSerialNumbers()) - diskpart = GetUserHomeAppSupKindleDirParitionName() - strings.extend(GetDiskPartitionUUIDs(diskpart)) + strings.extend(GetDiskPartitionNames()) + strings.extend(GetDiskPartitionUUIDs()) strings.append('9999999999') - #print strings + #print "ID Strings:\n",strings return strings - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used by Kindle for Mac versions < 1.6.0 - class CryptUnprotectData(object): - def __init__(self, IDString): - sp = IDString + '!@#' + GetUserName() - passwdData = encode(SHA256(sp),charMap1) - salt = '16743' - self.crp = LibCrypto() - iter = 0x3e8 - keylen = 0x80 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext,charMap1) - return cleartext - - - # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.6.0 - class CryptUnprotectDataV2(object): - def __init__(self, IDString): - sp = GetUserName() + ':&%:' + IDString - passwdData = encode(SHA256(sp),charMap5) - # salt generation as per the code - salt = 0x0512981d * 2 * 1 * 1 - salt = str(salt) + GetUserName() - salt = encode(salt,charMap5) - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) - self.key = key_iv[0:32] - self.iv = key_iv[32:48] - self.crp.set_decrypt_key(self.key, self.iv) - - def decrypt(self, encryptedData): - cleartext = self.crp.decrypt(encryptedData) - cleartext = decode(cleartext, charMap5) - return cleartext - - # unprotect the new header blob in .kinf2011 # used in Kindle for Mac Version >= 1.9.0 def UnprotectHeaderData(encryptedData): @@ -1528,8 +1365,7 @@ elif isosx: # implements an Pseudo Mac Version of Windows built-in Crypto routine - # used for Kindle for Mac Versions >= 1.9.0 - class CryptUnprotectDataV3(object): + class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + '+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) @@ -1598,219 +1434,117 @@ elif isosx: # 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',\ - 'max_date',\ - 'SIGVERIF',\ - 'build_version',\ - ] + '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',\ + 'build_version',\ + ] with open(kInfoFile, 'rb') as infoReader: - filehdr = infoReader.read(1) filedata = infoReader.read() + data = filedata[:-1] + items = data.split('/') IDStrings = GetIDStrings() for IDString in IDStrings: - DB = {} #print "trying IDString:",IDString try: - hdr = filehdr - data = filedata - if data.find('[') != -1 : - # older style kindle-info file - cud = CryptUnprotectData(IDString) - items = data.split('[') - for item in items: - if item != '': - keyhash, rawdata = item.split(':') - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap2) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - encryptedValue = decode(rawdata,charMap2) - cleartext = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB: - break - elif hdr == '/': - # 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('/') - cud = CryptUnprotectDataV2(IDString) - - # 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 = cud.decrypt(encryptedValue) - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break - else: - # the latest .kinf2011 version for K4M 1.9.1 - # put back the hdr char, it is needed - data = hdr + data - data = data[:-1] - items = data.split('/') - - # the headerblob is the encrypted information needed to build the entropy string - headerblob = items.pop(0) - encryptedValue = decode(headerblob, charMap1) - cleartext = UnprotectHeaderData(encryptedValue) - - # now extract the pieces in the same way - # this version is different from K4PC it scales the build number by multipying by 735 - pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) - for m in re.finditer(pattern, cleartext): - entropy = str(int(m.group(2)) * 0x2df) + m.group(4) - - cud = CryptUnprotectDataV3(entropy,IDString) - - # loop through the item records until all are processed - while len(items) > 0: + DB = {} + items = data.split('/') + + # the headerblob is the encrypted information needed to build the entropy string + headerblob = items.pop(0) + encryptedValue = decode(headerblob, charMap1) + cleartext = UnprotectHeaderData(encryptedValue) + + # now extract the pieces in the same way + # this version is different from K4PC it scales the build number by multipying by 735 + pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) + for m in re.finditer(pattern, cleartext): + entropy = str(int(m.group(2)) * 0x2df) + m.group(4) + + cud = CryptUnprotectData(entropy,IDString) + + # loop through the item records until all are processed + while len(items) > 0: + + # get the first item record + item = items.pop(0) - # get the first item record + # 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' + + # unlike K4PC the keyhash is not used in generating entropy + # entropy = SHA1(keyhash) + added_entropy + # entropy = added_entropy + + # 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) - # 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' - - # unlike K4PC the keyhash is not used in generating entropy - # entropy = SHA1(keyhash) + added_entropy - # entropy = added_entropy - - # 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,testMap8) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash - - # the testMap8 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 testMap8 from - # working properly, and thereby preventing the ensuing - # CryptUnprotectData call from succeeding. - - # The offset into the testMap8 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 testMap8 - 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 testMap8 to get the CryptProtect Data - encryptedValue = decode(encdata,testMap8) - cleartext = cud.decrypt(encryptedValue) - # print keyname - # print cleartext - if len(cleartext) > 0: - DB[keyname] = cleartext - - if len(DB)>4: - break + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash + + # the testMap8 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 testMap8 from + # working properly, and thereby preventing the ensuing + # CryptUnprotectData call from succeeding. + + # The offset into the testMap8 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 testMap8 + 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 testMap8 to get the CryptProtect Data + encryptedValue = decode(encdata,testMap8) + cleartext = cud.decrypt(encryptedValue) + # print keyname + # print cleartext + if len(cleartext) > 0: + DB[keyname] = cleartext + + if len(DB)>6: + break except: pass - if len(DB)>4: + if len(DB)>6: # store values used in decryption print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) DB['IDString'] = IDString @@ -1874,7 +1608,7 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__) + print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) try: opts, args = getopt.getopt(argv[1:], "hk:") @@ -1904,7 +1638,7 @@ def cli_main(): # save to the same directory as the script outpath = os.path.dirname(argv[0]) - # make sure the outpath is the + # make sure the outpath is canonical outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files):