From 3a931dfc905eff6dc14be914eeda09e0c8e6cd4f Mon Sep 17 00:00:00 2001 From: Apprentice Harper Date: Mon, 25 Apr 2016 17:49:06 +0100 Subject: [PATCH] Fixes for B&N key generation and Macs with bonded ethernet ports --- .../DeDRM.app/Contents/Resources/__init__.py | 13 +- .../DeDRM.app/Contents/Resources/config.py | 28 +- .../DeDRM.app/Contents/Resources/kindlekey.py | 698 ++++++------------ .../DeDRM_App/DeDRM_lib/DeDRM_App.pyw | 3 +- .../DeDRM_App/DeDRM_lib/lib/__init__.py | 13 +- .../DeDRM_App/DeDRM_lib/lib/config.py | 28 +- .../DeDRM_App/DeDRM_lib/lib/kindlekey.py | 698 ++++++------------ DeDRM_calibre_plugin/DeDRM_plugin.zip | Bin 353933 -> 353289 bytes DeDRM_calibre_plugin/DeDRM_plugin/__init__.py | 12 +- DeDRM_calibre_plugin/DeDRM_plugin/config.py | 28 +- .../DeDRM_plugin/kindlekey.py | 698 ++++++------------ .../Kindle_for_Mac_and_PC/kindlekey.pyw | 698 ++++++------------ 12 files changed, 973 insertions(+), 1944 deletions(-) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py index ddba7a4..7908e6b 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 @@ __docformat__ = 'restructuredtext en' # 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 @@ Decrypt DRMed ebooks. """ 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 @@ class SafeUnbuffered: 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 79b17f2..3a56e44 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 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): @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 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): 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 c5159cc..493f950 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 @@ 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() + # 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('/') - 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('/') + # 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 + # 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) + 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] + # 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 raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(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 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) + # 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) - # 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) + # 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 - # loop through the item records until all are processed - while len(items) > 0: + # 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 - # 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) - 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: + 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 + names.append(dpart) + return names - # uses a sub process to get the UUID of the specified disk partition using ioreg - def GetDiskPartitionUUIDs(diskpart): + # 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) + 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) - # loop through the item records until all are processed - while len(items) > 0: + # 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) - # get the first item record + 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) + + # 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' + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash - # 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 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 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) + # 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) - # 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) + # move first offsets chars to end to align for decode by testMap8 + encdata = ''.join(edlst) + contlen = len(encdata) - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash + # 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 - # 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. + # 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 - # 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: - - # 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' - - # 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 + 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 6ce1411..3221ecf 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 ddba7a4..7908e6b 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 @@ __docformat__ = 'restructuredtext en' # 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 @@ Decrypt DRMed ebooks. """ 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 @@ class SafeUnbuffered: 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 79b17f2..3a56e44 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 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): @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 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): 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 c5159cc..493f950 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 @@ 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() + # 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('/') - 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('/') + # 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 + # 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) + 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] + # 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 raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(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 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) + # 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) - # 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) + # 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 - # loop through the item records until all are processed - while len(items) > 0: + # 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 - # 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) - 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: + 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 + names.append(dpart) + return names - # uses a sub process to get the UUID of the specified disk partition using ioreg - def GetDiskPartitionUUIDs(diskpart): + # 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) + 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) - # loop through the item records until all are processed - while len(items) > 0: + # 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) - # get the first item record + 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) + + # 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' + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash - # 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 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 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) + # 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) - # 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) + # move first offsets chars to end to align for decode by testMap8 + encdata = ''.join(edlst) + contlen = len(encdata) - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash + # 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 - # 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. + # 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 - # 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: - - # 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' - - # 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 + 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 ef69a25ba44e816618ab0dc7077f0f5919970096..ef70c60e16ffa17c4b79875f93c0b0e71bcf7155 100644 GIT binary patch delta 35180 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�);rf+tk6t)R+wAe^HTa7`vu_)RtG$cQ~NHic$u#qKZ;p4fXd9F(r#q1sKbv zEC@evJbV!%G^%Z4MMAX5i8NK9J=GwMJ`EnVCv-4fLz=WUL8#O^o;_=2y=fVgGDQU> zF$bkZjS7aN8DUjQ%TmQtglG!TLEpw>N4YVe%UY)QpxtTKg>3i-J)*^={mb-!ElsYn zz_3>x-!36tJ{lgSe%XDxCuC77BV}1hA;8gM(j}$4t^8Ls)b{j)UM5{bWoYS=Du1v-ib1^V9t@ z1r!d&QSk>;UJDX+)1+&T=|mJLWO%CmAe}|Fh$=r-$A#y#Z6vc}-1Zj?KnF+aI~$vX zsvuW<@ih`X_)b|pHz}-1jI4y6M!|iuN@mX7$TG`1Un2qV_}otW!Gi`C`;`y>b15$E;)OS*E2@7C!w|9VvmbrM!8`qB*`xu~$uy9W1-%+KRaqWes7kBQ;i2>> z5Ll2)r&nfg3pC35szp|RG@YwU;T3kVI(Qi=)232krPS{A=O0w2hb%ob@1SIk5J;hv z(uqPGl7f*+$5z>1n5v|NP&aQ))@t1EK;;tWGFdp_@abxzms2!7v7VYLiC(K z%wwl62jTS)8Md)WGZq#YFxCyvIkydZXy`o`&Wi|JsUr39QBcFZ8@@ylxm|HokbU$N zHd@Wu`qkW|H?9mc50a*tkDHy9hS_g&`f1^N{pJJ}yN(F4+9dybPm69b<%QZcc>clU z_~6~v{aJ$BvWNG7U{yydmBpUr0GHN|g@tfp2d9?@xR808j02rtonU|VFCUvntDb3m z?V$81x;>5}q>{k*W*`67S01J2Hq?au6?Fw5@@>YcGV=Ng>~f{5WD{lTx&cR~kmXDh zse65V_4qT0ERv6<%pY@7yetQAgtWtPNd3ZggWg!W4AJI)`aT^MUR;vk9NfU-sVa60 zC0w_3K(ktad+)>pC}_*4QK_f1iLf%2qGPGQ-0xy?;Jo4F$cjkKye~WN2p-})6 zA1%7W_3`n4R1}9>JKW;#vkZ^;#@N&>mC(SUv3&j~saH)sz2{FO%n~uKB#m#X5OXr6 z>eNMoxuP5V(htgD^R^>iQ5J-pADf#BV^+%KzlVRETEd^v`qPHm zx=ZCBd8i#%DEuP~^}Ni|j-j5PhTK2cpPf_+BdlD1;asE}FzvWCHqsXl)(peChX4cD zl_Ks`(Vr!YrVz$xZvHYn)x&&4jsYT#;h}y zF*D^KoZw)%@#r|eI&rgfKToNh8{mSF@h&AFY8B}#X92)7r(fj@^zfMb1QcgH>d(8z zz}5qcCDyX8Fp#Ok&}17dEuNX@0i_#oije7l!G^hn%gad)aSvDtZGv%Xo`3kyg}B5b zWU=%9(pkctIYRSELa?`2(iDv)+}$Qpz%W!1ucB>r@Su6zJWOt73{B$#ximM)QRGO``6AM99gfxlyG=>9UF@R53FT> zJJ*!MDDC(9U0nkglpG6HSgEwU=U6eqxy%)L!vq)W7FoTKncQAsiFfZ47kH)F%6cc3 z#kuBbEh1#~tYl63lv!ngQ?IX-DP>4B?)?(=r)A_8N~zEGy7@}C4}x}n4nW8Z@Cu-c zhIl|-&G$@uLtUEpu6=>?2jNR}{1IHimkg=JGo1S2^W{z7p5ND$6P@J6-SSE>5T1 zm>7#ol%_-6ihGNcFXD;MaNVqR*J?RS_$Zx5;h#)W`8`ncikMKK$_|n&vSN8W^`cx;s3`x?f7qx}SO-^m3bYfA#DCY8VikAt~wsI1)p{HB25ocphao-2!RHwn~ga}?dyGaaCu2GGYrR*>H1 z$dv}YEf&W=*eARm$<4oi@~^91GnA(?c~ITYLdYQu??*z}sThD~qNStcP$VpOVGm2j zOm!4*;e}kX1k7=FP`p%xVj&REj`k0a-ZeX|cH8<2WaTRk@8&P{$l!ImReY~(d1kLx znpy$p*f`a1p$^3)Qj?Hjtz*TBvu^!qt+TtnZG96Pyl0$)mBQ7DTXfr) z7m1t0`waPV@F5icdX{2G6&Jt@@vgB4Yql-T9PTO}4Tji3Js$@*;c%8^t`}>YjC)~E zEIIGn)#aortPzZR=d;w_cR@3+!bu1Gx*z`h>yuXRr~89``^US(!4I9=qv*rU+SBv* zv+vh0R!^qSE;o*U)93NK&E&s+`ODw7UR=GMyxbnX8Na&Tc|Gbb6{_4|mAXQyCj5Ef zit^@{Os8H)w~7!VJ1^GX9t7>?j=2_oD#9%d!l76iZM;o3kEdJjZ?=E9+xh9&{a))} z&^|ive1Ez2Usvm|mI_5SQN)JIiz{%c(!5^$*eCtR9JHEp2yK} zkah*d(|WDn19Xjkj%s3GfZ=qKG`~yWX8AZdjPSf#uZrNs-T;RTFIQHW9-C9nbboJx> zvWPOmUDJCSJcnMOBY1iQw z5lvr)>6Ec{Wg*Hwlw>D+?}oM|rMN0nfSS*0UcB(+BV@r;dU{d8^rG0Oi&FW*D7{ak zJ3GZIpnUOA>p4-nNNTkvgACNUmRMk$_P&o-__$Yp~RHr~~L z6?p0vOx>RHt{FMX)7_a{ZopFXq#PUNu-Mr4lKSek+4Ow1y}3G!L|^aj+S8L<<_ujj zU1(k?1(tK7El~%U8DvvDPVNfcVf8zyD=$U7svqZ;EXZ2!Cl>6X_1eu2j>ov5jlwV5 ze5<~X^KqLm$qq7!)6I#cFm*DX$mH&Sd>CF-ZXZ!ly__Xg+Iz#1>hp@Lj1=rva@z)2H*V_ow=UC=av#c_)ckiUN)*>Kn5AV|gpHXszFJ z^>Kj!zpS7sRflr3>8ke``jr0s^++ivU8^dEC{zS!rZa}2@{S168#8s!OIk#Mm4L%p zaq|`n9K)vV6FIQ^fepEwQ1GOGD>6n?z34QF9OW&$?ai!q)~bDLJA0pnsMOX794@20 zHxoNEnlBL`i^%k$u{CPZXO)XSqT$3jIFB)|2-PuR4XJenhaS}zEC%2HDZmRx;VOF_ z6ff~v4$`rV=;k`FjJy`xhqZ`cFs7(q;_7v^Z;jk4()tC0i|@Jm2Je`Eg@ur*9pJl+ z6C^C9caO5Q4}CCcts}m(X)~)6che^0tH`QJ&xh91K(u&T@!Xb!w+fX`^G^B}sl0KTmc|2+8r0pz8g{LDSFA;2Bb*>OH`VETw|LLpSrjMG7^=9w-f z>Y%qHmSY)IN`m9dNO<K?p*_wML_^-1^r@zJ|Ruyi~= zpWcQ^bawVC?kD3^{yaO=>vCsj2P!=Og9@TQJCo5Kj*}=kJFE8n?963Oirn&-S(My= zfaD+D)fKi4VPuj0VX!}$K*e)jU#;hp2GDBQwJ(9kO=HaLpsA*A95H1vEI1 z^hz zpMU-NFTY;=e6WA@^Vt z#|1@}su@Zzibkrkf_c*ZeU4PVmANdFCXtqFtYmTx374B$d+k{;zdh&5ieecDr^wWe zXqfS-$(h=rS73TlcK8Hmy`OSy3?BB_sKPQQJ1df*raKcpa*G*dJ=V(sQ!xcYUKFfq;PQ`@j>7aF zf5V64eq~;T?ic=dy1Ux8qjiKaQvJ#7az3sV^u;nxXeQHCpNZEVqZZFB_$E-nO9(K3 z>*ZzDOS}VF(N=RZo)k7mvl>3=ZG=HYhbS-F$f8f|l_4t{Xl}^WhtjsF+IkF@G`@L7 zyGso#Rw(cNNaohOt+KDr=fpC_)(d^u%2l}y2B3&uZp}-J^2Ne};Fv&9Tu-2jl3J~H zcB+-aZvAZVXyxqV+36GTd;aIamGIerZvFJHXCFVRH{Z`<`F?u#+u85o>aVTUowHSb zk?I<#|M~Z?-@H3I*grn9x(l;<&1kr&iRlI+y6PIOM&9|d$zp%H`SSL4^CB6~CiTvW zSRwPr`bw_U6|KaQ=UY{v3&z~AbRfDk9&)h_sTcG?8#^Dl+KuDtJ$Pj$_>OCTi$&CM zgieiwI<{jqQ3T|NGTW8w%IjI{aOWvCD}m~*su=IWfW0nPpp)&qI^19gXSR+g6M3%J z8fod>XHi?lDg=@R6{aSf&V?sJLwPNBc$<~dOTKX)zNXezv#p5`@z>?rvNJISzO^iG z588&AQUI09VzHu}KM+Y?*EoTHMGE}m?v1E4@o5M7HeZ3@Jawy7fj$H0rrt8B&f>M* zn$mlO#FTnCzEy-L@f4wwNH2jo5+f`dO!BZj;sEb3rg*e14)NrJk{7ljPUg8d-0w-{A_DfYyLZ%_() z!V?XKDbOfiJ)gzc-l-s4V(9RCog!EUVk5?91*oI1Mx0WrW0*LOQ*@ih44Q?Eigf91 zlcm|{N(z^cuc0f1klwHbS2xbJ7(WyCR=W(QSso$PjPEFkCr6>WiESQZl?<$!E>Hkj z^HM{9aTj;2%*N+=!&c{i9XRIr>3&o}%IIYih=x*nacX1elJ?>-iCAWAN9P1Wdx-!R!mlBjoYYGf^JC!JN;aoM%!$OHI)9P4x@bA$-!G^=Dh zxx}o8ci+%coKMy?f@g;mZ`EZ&SXcwNvN{YC#O?TCacX-S>+p+YN@N=s1g zFr$!2jV@k<2E|qMz6vKGNU28Cue}UUzcH&~=s5 zy=Of$kQ#3HAwaAJpWnGDAgU@!)ZH{?5>&ZD544iag1Tpa0uJQ|X3IvDt+!UFK?P0c zR71C)>vT8b#v948wrb1PaWA90@;0D7S8+ra6f3eKWXh4v`UG3 zFB(g`;Ay;e?4=w&In|mR*CaTmL{=jjuu(l_fk)$e_+;g~&590EPK#Q#QNC+DGDj~e zo|)iOGx4T>HK0|9Kdr)=;4K4d2Xoo5su#1R!|cE~H`mo99V}-W32MK!cGKT$RQOq- zSnanqUM@9NNN>G>RioH&kwNSUaCIZcpmkujYs`@pVAfnJ_qVk8p_j}KpZxp3MMI?8 zNXO}&Zox#BWV2NLBAxa0d@Nt3D})Z{dnpoqV8S|oyIMmyM34B5RZ*UbM(UledfUyu z7lH$*idBS%LF3@?}+&x?!I{b%b>_Fj0~DKsuK8 zviKquw&g^$Il~lIV~hDT`j&)AVUCk^R|dw0sn^tNTR%WXGH{v2Ir7IiVyk9Ld7;b~ zvo~>nZ4^M%diJ8Uw(}Hy!$$M*@k<#=T?ogy)uhjYwTs~t*U{^PcR#&7;hT2(M zu~q)~YX8UA?}GL^-3R76T`7Y+dHLqxMfdpm&qv6{>V&h4q#SVI&Es?8MEqudmS;9f z#d{DI1~&xB0C1=4!MF7sA)F=o_#dTmki-OkW#nDgy;lUfV z1EhTXO{}*R&T!_8_FKfkzQBIF`iip1y4x^~1m8;06<<@rCrDMoRhgTpui0I>u;z5A ziU-E(472K8&zOMUt_70F_HfW&TP`=Y@u*RLCCxNS<05rU@s*chJT(J{q=HKW3UZ-e?e z{wcEKikK0BCI8j5*0ZDQb)y9Y^}7O!V9uv?9kkmBl+iZv#brd#XQO_=h^KHmO>)(t z%B@s^t!QBV%K>9WGn=0!J*VszfXf4aXfa!Wlm;pmu~zz2YZTHGSZ9Wm1;~hHeI-Rd zu9O=r$zx#WQE+@OwM@9IwU6^v?txOd(aq%XWJ4_GWZG$339%~b!kxxJ%b*ep1gkU% zj9(&66{&MPP9fIEfTbqHkPYvLg zXNXZ}^VPwXmJdj%*`H$a_FyKN!gXADzA#ISjo#48;ZlXiWn|?te?nIE6&f4Yx6Zo% z+k?5*ORf>p7Kw+ZqGlnym|e_I(^37F(YZu+qvajdEs`gvtEm>u$+wf-=yuA3l?{_k zez6(b4UMjQETq0adlND^DVRrp?prCm_f_8drOMjP;yHUn<^#|93UrCZ9DUGkh$5}C zv?0ABjrq$NL**LRIb&eEIb0+_CsZ>;*O0zpVOc$@iARmZoJ5jdX3l5DfjS>C=LCgd z*G$4&*BA++@y&&%&qIlJ%GGQ2uVo*9IdIU{Mgyq^9AI&3y`n$K2!6Cu;n>!VP=~_SM+o>0N950O`7u(I#9L#oHI~;wS>(w<*_Tq zy83$KL&84!vS8MX-gtbKHa{yv28?~}q3ja1LJ=g@sQt_Y82z?RXz^j~>A6k?@qYIG z{`$qQt0&hH(nv6A#LbU?2U*^T@NY;KQOi5|RL8X$eE;R^_phEDy(_Kc=mLLorUBM%n2k>LXA|mgQnxKt|9XJSmoXxbXKC^{?ZukL|LoxTVVqu(mPyT3hyG<&AJcnwh4FjLa$t5%3n*YVnXd>o z-!hU)*Mxjb(QvqjKt`;YNnxGbQGSd1+_MWle|yCDQIb^O_lKevy61y*k3sZS`%+Dt ziW=fQLEy|a4hkVttK< zInA7_RN%H{6PW>Dy$zFT3^k1J2k$6cdLB1kclDXPZwBJ z;SYn=nQZN=t+FV6 zHN)1WALs%A>H#@7WyJ>)EXKo2ROqfuOYWM zDxf1j5s;49ChVAmdi zU9(`2B+ZJs50oaxW+nNJwgrN3KtJ%5p{!15erd2go=iKGq80B%_1_p@(&Uv2bWU~nlxhh=w0(a%>{$_i zxAXFoW%U#c9Zu-mTEWC+z1PsYOAS-7(u4d%p>-2nRPgfYOux(bN-Tw(Gr27dhdA{?xQYd1! zX(r8V!{P6qSQ@RlGDv$9NTYz+vnrCd!UKiSMl+^af<7ER4c?~FY%ng2s;Cl^XBY<= zr>pVE?DXpxI#rdvH5gxtQK8lC*-|099gO4TvQxZIRwXV*lnA`H+U=_ENy>VjJWRj*3It%`8>A6 z!{YP!F9Vv#h#8QJBTW+xQ_o5{M5nlo>Xmssl?zT5Vd-)~9RQt21_z$8ohJ)Ai5Aph z({nt3fh<|eD(qIJ7!hOL7rA18N~2K4gyGs-EJHEtm70yE4IGVSA}6L05E0`c7vcJ- zAD_p4Ty55m>^sNv^XPlF3G`%hxg3{``m;B$jtcff2p08K_JUHj5*{cL^Dxwx@CNmL zak7sY_FOatR(;ry(di4=xCraxmnwSn@qx-JCkp1V%<-Bbvl=v2RgW=$&>7^ubv52G z_TQ4}V-2ShN<4PuL#d`TFzA$-ubOKhgDh#1B+e5XTyY<*KqQscz*E?rGhq(N_3@5& zv)x*Zr2j;O(g{RXnLuPUJB*%@Z1d|;tlHEcPCJdl*CAa+sTrEBe?6L6ZT#WHD);ZJ z6U@-7)9idc#Z(rTe@?=GwSrzG&R9FBceXn1^#u*agH$vI=>HpPE|lA*(%NIKExtIv z4Lq|Y% zNjaRn#u#fzl_(>1bOX-(5XuW;d%KMxh3n;|GK0HnW8rl7peua_}@D6jL=GY92m7t0`Io=?pr zWS$2z3Yq8gG7IIUKPkhId3>3M%(H05A@eMhdB{8uXCN|fq1k%XSkCp_XU}c@=CPr zTPy>TdA=kgk$D!&Ok|#@oOsBWp~zdnWGeCzVqhnKAj)LN3Pz@szV+rDGLLWC7R}E+ zWS+v_^=wex=qsl=wV&mnkgaDvkSF1fc`7-Gv={G!#pBH;R4p|FqNo3L7M}Oa(Ls{Jmu2eb?zMf#EAnhlypfDbZ3QT4r zOg1rp4KR@vI+Etiaqwf)J`=040fPVjZ#??SmA)%nPo$Q1;b10LDUFxn33S6uo&9iN=QQ*E}lv=Q6r$sJ(A#tRJT3 zi;IAZ@yxI>Ua1(5yv@XO&;DaxIy2BKOEI5+B>*rB;_?wxx6?-f6@9PMVRZQJ6~1QY z`hY~Cg^IjR4D zl8#5s!R&gH66Mp@$FjCCvV0ugV55Q56_)v}^?RmvG!FB1f|TPPK~PCZp}9+QmTPjO zR(r5LQB)5Ay(}}#v!!f6Mbi#`Z(gm(SqDw^t|S0>N+TQH#pyK7 z$TG*GdFD=^JG^)v7-Cl!K5K}zjOKfPX8MH=dr`79OGBAoi|2U(O<*Wt+IuP#r)1JN zsLiQVa8N5MQa%(BHt{X`{PN?s2gd)wvM=?=QnA_>5KycirtFXQJNxSXs$gKQ;%-0z zgeJZx$`V&)0Ofk0V+D^qq!eMAYmO}BH!Rc-Qw$in79>^oMa5i(pyE6pp6oBP7S+ zdGvmy5-3E2dj4gl0E$y3ZczE6L1i{~vuAE)#2?188I&wh#6Bfn)d2@3RP4HNVsP_Y z`}eca!FY0?#21&-;Q#)gpwnu9w^rq!P0Uje%hjDtuy=+mW|IjJ8xKBcpTYNTJ+qpeGe>ZG9_b%QjT#TY^Wfsb&5qBD*W|}kuYnaL-v4xOiqWZ z5SDIFC(MnXNgUmjgCAiWq$;CMwMA;>s_$Y}Qhi0~*10%OuIO(%-ZQURSXfTi-=$QP=o?HJ>J7@vdJ-RCh8S zPjTr&M@0E7}hG8!n%8;8HUgB?E5H0PA1sg?oB1D+cM!W@aX`#ew z;i!K(PWCK9H-Atwllc6;@cQM^(-Rk3!gKvAc;v$Z_>ZD)dC%)!dF`I@&U!V(bm&C> zR0-BRu%1?)yg7M)mNnGQMzLC4JO-JC)$OuJuMhP&DRxBxuL;HQ*!W)AADVC<2T6Eq z8(bw?4%4fm7m9Y53&JV1uoc8y6qV{MXr=Jc;p@-$^XK~b5BT>U-#+X2XFK3QozF+EdO4h}qMEqo7j9$j{3y@a)Nno)C;CK2Y>>>}3~`1FJig zwy6~gjjP*I3#}khLE0!blC@wfP_q4XS7b>@RD;P0hlLP308HcQ(CI8_@z2z>Zoexn z+H76c!^|OwFpESweF24@>>km`!v{3=J{yZX3+G(J@az;q7KcU5bS7cZ4Gv&}FDD86 zElc5Bkw!5OXwj=IUr8~YY4AcH`rYnfhd!Uf*%W`}&*xb<6RDc7;H9eHzf72j16drC zg117UiPZ~whi=e=vs7roVvs!MOmREjhm$Dc5vU^1GpP-lDNng%8htT#aEOAP5UC5L;;!r3y<@MqskC;XY zvQvMgf+XzR!4_~`U3oZ^T^pZy#xpV2#;9p5WiLr&-(C?ROZIInW6W6MH42GjkR)q_ zlawVCB1 zaUeqp;8GO*E|24M&#!zvO@DqO_N0=1%*$0mMnFxTu#%=*f7IjXO)kPw$kuBP7bfw@ zKB(uoUbAt(X=-G$kvuz5&0Y$d=2EsU+7VM`m2m2Ll~iwxxYko=NDd=qi5X1aNm_e} z<1tP!lHy9^zzb9{eZ|crDM|_^udWx(WB@bghvjYtl^<#(bnD`7t0cPbn=X_N7rS@4 zp3*lU$X9PejAbXwnivw5OD3;de5Y$E$OtNg$LT}g4I`nV^j3{sDLS1qrrlbTWORhD zPIVOTr_HVkw4uzMwFugQy)wDAnD5cZCy~)xRrFJLSY2k!@sr#;IuaVJ2I3D}DjE)6 zKCAMkhOjQ|?aRj+a`daTb0#mns8}cGa2_LKTTi3!jpZ%lQ{HsfWvh;`J790+uEg+E zwf+7Q)?=eqqE;H%1e=-rvN5JSM$S_#>*B>L0k^XAGm@^>-Q?dba8&(xBkr2#OHye6 zQftZd)ih->X7|RAN#2pFTB)|6C|R4RSlr!Gtpeq)#-}>B6pg4AwwEm?YkM|X2Ey;r zZO&;wqx}xn+Ue^#4Yn7SwF%Ju$RklL=;W>YH-e% zN954ZWdmwWoo4hx?nbASqg1@jj!f>Z^u{MW)kG)b^Kz$`dLmDp!#G%+mmXHOTUXDw ztH!!k$}vsZZj+w)!q)hzyeo^Gl2U&YKFdSlg>wJhH*UsOnGn~v>J z3Q?CE=8K$t(O3TYy_wjKYbjoVE+$SS+hSSctuy87xcw!%H%}#hGU&6dI<~WAA0uf% z)xs?)_ls3@!$4+vUP7nLJby`0tzB%pQl7V_oSVa{dQ(MV- zBK`Jyo4DnDGZyXY!3!(vGWpcI7AYc!H)iiR-u7rgD#(AzWlS_}DQ^CpS?iqRchvjI z4z6lperCU(8u)tpVR4vy+o z(?SYlB7$d2m8_a4-eTqzNast`&K(e2oUn_&jK{UDwDha0(Q99X`K%hUX;zCoOCkn; zu&21r%x&1ZRx{MIvR7o9nfGdrm)=YrbV_D^z}e)pdNSo*Rz1X*$Hi{knVDdon|}AI zd?n%`QH4KZj$@c-ls?3bJ>PsJp(!@!UCo1~KMxiNvCSSXG@_nCWoVuJ)lRfkc%lv6EyER%9t7JtgOTTo9kE_LzLn5HZFstT!b zDax2jF^bR3=QZ`ba-Rv~`ysp7z4+*C-FA0^EpL8U)bdaLrpeXu-IdXSyyj#<(fVo$ zY>Rb0&x=4IvD`I4(vF63 zl2Sgz^^o1cUNd0dJ`=5ghk~#h`e5!0?23?fE^s zDH`>UKmrZcgPy$yPicsGGk%d69e>RLE9#c!JUsF4 z8aFviJho%lu_t3YIjmy{6xouvqDU?uqmc~*`!^2Q{dj*YzvQZ_yUFH5B&E^V-XM*{ zBaz+J)%C7ws=5Ck2U!vyjJ@E12UjdwrL!=&duN~B`_FsK30*IkK4e)sx%X$ha`(>N zJCitEu+eCerCH2JBY);CqA*U`(o1KfB(+n%;6Zx#&S>O&4i6IS!q_PrJiU0quI>-; zjvG3YFkaXx+=DwYPtrJZ5T2Xz>y+_efHNT)m)z%e!d;dHE{|C{<1FRzB4Oc#zC3;T zmObSGk8Ph_WMi02E*CKiJKnX{SgQrAW1%owc9gW-P*$G99P3;x zv&hc?v?cL;Gk^0Ed5OWN1-F9~`@7tUR}m2KlG{kGi}UXilbyNYn8Uvu?*NhD(sykR$^Ft5rV=gNZlIVmk${ zKBOx%?#vlZjWiVm86-%_q9aRqibPG=c*PuG`CL+%?LEqpG+cNe)X+VIInL4)MlEN+ z{#BSEf?RYz=jWF{>?yj>yy?t`|5Ff7LrCXE%qKiX;Y;qJNU;m*3SXW8omjex)J)0H zVy~cId4Gm7-mV7{G-V->&cuEL-$$4U2QIMhxgRm#3t;tu(26i(F`{#ChCE6VrO zsYi+cQj>=9ib%)U1MAQ_WcS#!z)N8&77JA?ZXUtS@3JX-;#~uiQOJJ=AqbZVJBuPy zNd#X1!FEV8^7|x3=b#P1O!gS`38qg9$j1Qrl7AyIKs*Pf1cVm`<1*M3c)hf-tyeCywW_43##p7hFT>qy&Z(_+Cka36>p2AVIda z9M$k{KY8ZREmn^^=90iYAc zF@JYJ0kgm%X+bI$-TwsLAICAQ1f{`8iN|H(xJBgil%sNk-AHZMPVHjcpK~hMf*(05 zv=9(7`vXg|2u*%5>Mt%3hzsgdifwu1%7m z=ixYHfd8M0@!yojH}w4reOrKiLw`{LJ1{Kp_@5^&@`JGdqPZj z8-dPBO9&cPSu1!S3;{IKpOmB{2^XA(rS_CW^$qDXCvIECm}6}Wtz+F7LLAg|#gYYD za{Zo&;`a8yv7XD&1Y9WKo=->j?%uh0@%HJnm!p?wKRkwrFq5t}zy8zX%YWC;UcDT> zd3*5!I~Wg4c4D&QVYLShw)U*QgS|{-d;X>tX8G3@sDgULKkkq zBoymPBbA_|3*rBm=hsqiQGa}q(-t>l+u>u|najXjp(Sts)r+1`_q%sM$C88*VaYQX zkFOn0f-;1F08LW_9r6fY24t3#$4C++!g~XejEHTjy+e%Of;2bAUa}NM$b^q!a$V^^ z&Aj8H>nNO$(r^UkYV`NC(i5zSpI9P7QX|B-k?X}OUHCb+!9jxij(@qm09nTvGsOTg zf*27DD(IY_B{S5m#RB4dP$Zws4mo!p1}yA3xO_-O#88Z0r5GVpiX$;VN$iOn2?`gY zRm>9P{a(x?A6T--#(aWmvjoGYLV8Pa;WdnY8;r9FhKsHwKiGN5t1wm=jf{l*6H~53 z@dq1Tx8(9p<^9TCLVpOSRQK~MMMrN^?RyHTK)|(`n&9nGCb(DNi>u=jS&SrfRACMX zJ4qfKZs1*0QgT?C2JZoM{3B))AoH2coT|e{N>~C7LZG<=aorf|ga$6!X&UFW#xFWz zF^7aSV6vN`L6dzeN@bK4LxDnLz#3Y~9w@v-sEHo3+%?;~+tq;85^Yx+twOd$g+*xEw)SQgEa@=I#iXn!-p(=fyhT4Se*a1CtHF zR^T6LJqGW^P&%Tk!z6YQfuOju)WWbc1V2Dn#||(k94kvJ0swBu zzNbaemY5BV{0I}uq>3S=yT-c#kFxQ=97OJf{-w{c1OG07_4q~nu|F)uzenQFeevf6 z{-n`|!F#DOVK9;=w>sOK7_$`o9K~LUL0qK^^m%Z2D1YoTensVz`4Bi2QY&5SC>~H> z&R3v4o&)iRoB4u5gA#!b{LrSz88yOI&d(|!yhab;B`gLPfa%N<(+qx%UQ^TzAfbyx zKph-GF-q`cl?!!h$7u6ky(W3VsGWIKZ^4@b+n-Qq6AnPK$KZ@0FM})_(wzdL6pC7H zBJXX1aDRfk(Sa3^U;!{wpqbVHgjWcN!~z0`$c9M7u&J*I$R=K#U^Oj`S3?!R#95Fh zjhd|#P#&GXcmbLVeo0V@QaL#=$IOD|3yroEQ?a3PNy<`!8~7_oKBI_Z=7*(@sE}e+ zy^CZFpbD$Bx34b~djNq?!%!L&veT$W#ewjh{cVjHY>AxHMEzHI83tQ2Hel7V1FdKuu;sDFpI&w8HCfYJ`8+RkV2I6@$eJ7 z`gru|Aw&|T>QU~rr~pYGQBPL)tO;$o7+TnAvYLq0Cq^eoE<{B5=nm8pEQq=niP0n} zG!kPJ@=8roDwM^P?x`6<-H7WAV8#>{8~IFY;BZQHTR^GQx9MI765!y9`OU#{JbxHA z=OcRSMyST_JY5%C6HW{Fd-8hS+<8sGA}mILtkKea9qsnjjuw`4br)`u@!{|(0hF!! zNImf%Ntb8AD&vJkMRP6xNBDHD4n@CfG>Y~-*zQ6@0 zDd-$YN~v6Lm4=F)HI>a_I>GGNnL$cO76B{@<5)&Yv|)-Zjhv(4{90#&7fh59OcjD7 zM9awbESyny(RD4XtH@70_9mnih@L_hdhsb{CR?b*1U;<4+=BoA>P1G0-hU=8!a|!} z8;Jdljl`ir=V=S)m%+#5PrnSL-XmiwOYE~Sn9hPUCV6-GzDdLiybNB_m3_rk3`*qw zGO7sf$@S#C&{?}piD?dAcG@+_CxA7T;Kxa8Kru47{=AIy#kUL*_K+$2vh@*vo-iU)q zyHB)@7-$@SHy8uM0+gb``<6IDLK!XWWDav;okv#pjco8g1LT8oZGU-?5ly|+7!E1i zW7k@(Uqa{=QoV`EdOR+#`Q!1!rFgRszPdVLCVEPZNIErBSDkAZdys5 zpHFNme`*XQW0NbI;^+O%`B~OJvz5D5OBnHZW=9F+<}8_onSbxngEo|`W8IN9eeB{{ z02}vpV-c6cG)c~Ld?K8As2CLw-<)~1= zr(vG*^mz*wqJJpbmGEYJ#rm*Bu*Q6pm!!>Z6w{cv**$)6vS;d!l?oDwKYsVFsfb^m zW32`5w7%76Qp&=7YqDI$bB!4;cLZhAr9&$tLna#}Y_b>10EW-W0Hby{B# zkr-K26?Y8hvr@!*sO^^xnq=D`c9N|b;*FG zp(wIxwtvtZpcfvIYZ$?mfvH)$4d$UrYF@(Y8rVoP2sUdZyMg>~EB?JSnOd7VCu?Nj z1`FC+p{Z@I)jQ8ZRO`-0RO_9U?2gJC5xKion6IRv_+DS#TOTH(XWiJOw$R2kS-%AM zP!O@`$=Q!!0aJXcW{|KkBt8$B{56f8KXU`ot$(!)V;86*ix0kT8tHrxFM7dGJ{)iNC!PVlKE_hsHN6AgYz17v0*LZ|*ZU@RRLf z?|*t4>w$3f-qtijh?F~&opQ5LWzQh>iMe}o>-z6p6|Iv~VgyikUF+=cKvnCGl6pr+ zpKTpgKcW?D@%ab!tHE+qjK8b_=v}_BTAwZd+^YH;#xGm5&3^*+HI-}C15hfe$=z(f zYHi;AYc1F64|uK7soup}m&cnHYYA0ENPkW;62XZ!<-5a5eRFs9T_JGKZQwSC z6alN&Q^WPP<3ezcC{nfGE5VJevl3hwL35y)>7oo=wxc}?jlUO#+qL?)P#NyboPXA= zZHv5P>!s~FTU4z(FH@~|ezrSF*oay;t6qcU4DmP$6y_y5>17<^M_f?hqbtPxz2x0v zeK*Cf5eR1UrUZ=)XFrxx8^hi>-2+t;5zOYM2%Fh;do4`);DI6tRh$cp=gt_3_w4ZX%BVTl78QWUE@~`~**Aq|e)xOC!%sD90)ye!Mi$ z5({xOceR(iI;dnIwtqMRdn9jm1u%j?kMkAz&*@7ng-}{BEZVK z#dSFhPhqVlR^rI6b33)MXj`R2exR3DfNd{0P=J5RG}c*=Kj&MO6m^$_?Ft5_j;tw{ zVj|njwI-_Dw2O)`9+L3{z*T;j-m|b&et*J&c=={irZNL}BsgIw^cSIRyu9c{YI*!JOmUDcl6s9F}qmPFV-NFEP ze>`eHvf~H%C4c+f?}5vA5}o&`2NHC7KzF4JTZ6F+GYBkjZxKz;$NT+^TWKy7SNO13 z#o_eyNStm&w`=E@Vq2eeZ|<^GJy3c?^WA&-U#IM0D|}ILLUgt5v!tb{k@Tx-tgUE# zDIQ>}R&o_BJERj^XIVrO_ce8qwJhmb0qKr3R1@Yyzkec+m6hyo7`6{R@TFHV?&EV; z1otUg%>eR?%$dtv4C8aIFzBOsTV!cpQwFNqjx*7lB3KG)*4D|~GI zH1v|3>VXljQLj8V{dA3nI6k+7EYh#nt2E*s=E?F+27Qy{-KTRrcH`q{;cu7A-O8gc zlOBihfqxPr-R8XIDU|sw9mRKhPpdQ^!`Hgov+zH3KD9F6^)64o|DdF&w13mQZDW#S z%^6yL9H_Q}_4^ks4{0b(>#DF;0jkA6%3pdQRR*LJL04Aay>rsw<8vhal_Kyh{JOH& zNpe+ezrKf0dM%?s^Ec;PckEMlUfqqeK)!^iwSS;nyqZdXE0z9oD@Po84QmkZrxKDh zkZ(^K6u_`5o1`v`JuSbB$|&v(Yu$MqujvkVIz;GA?Fg#Am7fisxGa+y464Qh9X%XJ zZ(}X$4@u;E`10Wj@OMY=Ek9iH*np1@wOSWgtsDxtws%{-UZ-N~_GP8upm;x1xkGO0 z1b=qQkJWbv%8Mv~(CZb7)t@qYH&6`?8AkP*yb^M)n;Ba8=rCM);c@Qpx0!z>}i(SAnP|vXR zqiuSEwQy#3Fy(Hpa%xHBxWKDIOWlfk$w;ws005u6mw(6s9e;Hb;Ju|?(@Sx;}oE2QQNn)nEr(e_EGXv~>vR@VX{vuBIdHRZN zw&hute*DoMyY<&wY?(!IdU}sl<;ktj@t2Q3`uL-hJX^E*{G=+YoX_VhUT?CzWan{t zHh(Y5u;go=me_DUPvRv{3vBd&jlX>UEqnF1G2Yo@$7PsCVIHyPgpjSXs7iPdeEg9F zmlZQs=HZer!sTklek+hF?D>5A{mZ+mJ)3MoY<$9&VG=KLJ_QJ6zPxc+?K;(W1bCQ>!0iehys^mjv!<946Um{Xw7+Xn)9) z>Bk@OizVNb?2ra}kmp%$Gve-l1|xhbpoR?$2n+c4%PJnkv8EsOz?;$tevdaNaeu;9 zga3;+U%*$PrcxhkMFn*Sb4tNL(h}920LVtcX%&A~ONAp9^GFWyUllLPI7{mv;`Ah| zzhoPp&Z|6WWumy2XITbj7{&QaeMZ8I`dbmc5iU0^diBA=U(KZd&@p8t* z&jRe%$KM@2q3;%ideEwST%5;glz*KA_PEH33&AhNC+b_C#Rc0W)oGlL3&m!OX>)PE z2qGTk>tH^Q)3|Kh(erPA`10^*e)QPo*WP)RGc%O zY=~8J-~#W2h0Ke~nAbzEj?4VXdV?dDzxX?Da(+@EGWyWd7&I%-;%Eg-O@H``Z?T{K z8)950VNoz4BVm3MY5Y^E^ASH`YW$OeCnqy0g!v{c&t@!4PC(YGHC_@xVDy?IHq`=u z2s9`#6KLP8b^-x35g&9odkpY^VsOP1qa$%baP5I;5Ip5iUp$*X$Ip|g327a!I15>l z0YgEFH(LuD_?wLZ@sHP?Een1gFM#1G@VOZO3YVhG0hJw(qKKVsH#|?`bj8XHOdFdr znwvx6H1hcLtPI#smsHFFEPuWa*npi6MrUQYxwpT6etsUDWEp7ZdVlorIr%#F74*En zA3hwIAHrKS;3x|f( zbY4(gs^R!?01l01vZ(>awlkVq=AsFrz#uiyf^V;J>YYgKvMhNSZ-CX+=n1MlTG2X8 z!&6Swv6C!kkHb8bfq(2#28zb`BFk3AXxa-Mlo|lH_V4PhjL^_K(EHsmPa7koL_LrI5NsQ^0>>xn*~ zQ46I;N>0?uKB`7LUfm7HeySa65sE`*L``!7Xa`7$im2#ru~%&jA=08<%g(T-~9vP4!GDEPk(f2OjB7i}x*;Y|jBS zOJn$c1kGy+E&g%0*X8LE!A^v#Xr!G!qnJw+9*8~~fq&~I&{N$JB(N$^3}T@%sK>ln z8G*?(tMVnE=*{rjV^6Zpmdb8ly?pj9m+Q{~Lw_xc(}}*$RgGll5q_pc2Kt2f;Dfn3 zG&K?65;-l|O=?uL@%ea)D>f$-u0%e>+p9mtC5qv>HylNP>}%vm!Z@)>exfp7%I zlI%Da?&Ejce?U}43mV|_em@EDBQ0Fn4u2Uh=`DW8Bq)p~mgAClM?HSM zbgd4;wei1=XLH7JBe#&;W1Lq2<;55{WOj0~DJUtG-En6+nlp@Z4|M9IO+xCC1m z{c-y>`{T~*dr}qIAD_K0Mgind>#&@3Z2y>fWZX<>c-<_8zKx~On>Em@tbtaj?~Q)@ zRQOu2imI+eI8H@dQk;4#px1J!oqt~6602lS??l78-HxeN({A` zws6@NiUreE<#`0!Q2N*Z`dH|6wU;uUQUi7do+&}^1d0xg-IWVJBvT%!UJz`qvsb)L zEG`r;3!7$xx#&PP7zwc|O4SH&QK_dGG(<5>NLZDjyG`6<1+oGU#B!h+I|`$tyDg9x z)Sy!!J{@F*^C735EDg?~xn3Z@I1S2#^G3re?u9O#CCYAX1JV*h3M>-07)|2TZ* zuvWw{0mgb5!#M6hg_mBQdGVP7z}iIz*Ifwy)xEmeE=+fT>jt@jm@LPMr{VB%{oCZT z+cS0-{;E6i@BDcNlx>4&ko&BwweQx!#RlvMrm!xvCGZV)+fIpaEq`K%;4sZfO2sUL zY(OI2fOYbBi#ZLL-hjgdDDOM{`{4WIz9$@mO`r77oe;Mg{d9PA@chNW7l;3IZ`6y= z68L!&^?7kYe{={UFcL>Ce6}udfDl8USYNr8j zl{D!?ne%da<}5@>UnFxM>uAr!{vmjhWQ9|8Yhb`k_!|t%9)IA{>eW4@2Hez%{08h8 zFc-~86*Q&X$(_nFT$RR4%uO7VEYTjP(*QCSP$O-Cs-r02Day=L#Y0(lKxPq#3)eVV z$0@VBph_0rvLm$=sh65}tfOI=PU>pxS&Qw!d?l0(r(^=)@CcXaghti1Jlk12%W8N5 zZzI&5Dgk+6wSO)OuyC;fO|>ZAV@Hq#;pk3z%EjhqwhYrHPZFaTgO?C<^Bd$(6j{6q zlL~8bQBZ%oyqvM9Y{2yiobLEVo+Togl4aGO>3`e)otFgG0ZxA$WyHpUQeRQO6KDeVmC67i@mq;_2I8Q+C_7nbc|$;=pQE+ZW{92) zRf_pU3`-ZH_&CP{YBIHZ@I{yu4wa5zaZ(XAhHun%k~co72tGFx3GTaEXIa@Zp#Zz3 z+cshlkJMEx;;4yrmYD$PwoJ!-^0nl^Fd^Y4jEq6-1_XbHNwQ_5}U9 zAGaaR+~j{n^Cqa&98_$Mi>bj`x%Mk=@BDbZx4MZv8P|Bnn<^SmdB*X5;IqK{%%kK~ z$xRaupsz;qc@F4|&|tdann144PsVIKW8*onaIXwk9Fz>WgaKz*l^Jw~AQ)W|*dE7& z1(--AA%vv0>NW%}!wuz_IjsR%Zp6?r8UC^{XmY z{W|^C0Lc-c{-hFR=q77tYA7>B2R@`C5nY^+YnfG8=>^EQHK((RA#RS$AHjDL2v4yA ze$5Bc#1rh-@SE-na3T29q@$ZktE{dCBu6wuw!@lwvu4eHh3)1GVZ2gk zo>716WT>ixY%MmHdB|2JFl~F%K*+}TfTt8CjL-PR*n@^A3Q@hMX&@SGPmZXV7`Hl+ z=-Z%L@a*Bkb6)YB!UvT);xvK&K&6ye%S%s(5o+>-a5K^Mk#_RQO_D7=@luxf=>vABy_J7TARiHlq^8I!(tCfoM5gIfUMW8LB|Szw z=pGRO&B)&HKlmkm(WDS{aRxhK1xC8?@HC9m+SwVS?yy#M#pwwQroFr|EG%t@udex| zNl<;kWmokE@ztn8r&1)y4(M`0Yc7j6tPJ!|X?7wxxff{kbSU zyfZFAo97Dr1C3SR>6VSpWo~xj=^()k%(4iAQHt>t1A~Zb+18cP zEgc`FT2Zy$6z!^g-+&(|@o0#gnPz|DvvZcpf8>f?=FC;=@gA9l>Gt1wx<^(IO&6lk;@wV9 zqjV{fck9h&nv)63u;sAE*gfIm>@G69Put-3?)^8LYb1N&bd12=0Hsd#;3gE%DRzX; zMGej76>>ZAZeFabxvf*6!z~k{xjpWHqN$KJQq-v(JukS~KXF@&Gy8wEp^pnDEd~SK z9gd&fHW~=0yW6g|?^bxC$=T~hi?ue3w9RgzTYPHazm?sggh>JZNjITH4_7giG6#@PAj!A)g z5Eis??LNCZfE(+oATED|CA%L#guJ3C@9)PC*QLOc(J;adV}-G|B{RiyNCX{JwB zU4gr2>fU3=$UuG(sza0z7PBZrjXnbh5x89>&(uUJOwXQ7=soE4V%Ui$Ic3Lesx_3P9=gEIH3{7w4^YzwEr3Jcc zR4((Mn>^d_yxcY(bS3Lpjt}*17Ir;ky;a!$BcjPi_JeJ%5Kq3nII+^UbcmCK7_Z2O z`TRwDl+O_9zRM_)o@ZbIypCZa>J2qut%o#PV<_3Q1~K)3btBGd^aJJAP1LD4H7x4% zFxIjF>r~(U%|jm5#%OzEVuvyEZ>GXE!k2VAbF?& z<)C&~DAJoOO}6^+cX>vD3uPS$ZzWeV&$INGF$H}^ndJa5Ot(|gEQbpG*wl0(c1TK>;dWIjGhf?!SibjT8{-8_m5nC0R%&+Q|A zDE7%u$YGfbVBt?}<+y#KV>SI*JHzHHPB+_2Q;GQFyy)*z-Hu!GEW z1M$OomPf4?x?JvJpQ?4Mb<$2fiF2sNqF=8q_Hw59Qb+qPjS`e`AgnHbqcn`jCt!p5 zR@*KE8V|M~&#N4Kz~7Yl_+Wd(FICXFb5qb6R?vB?*+~ZiTDEsEbi9)b^afJ`=zD^P zP}M#j`Z8aMw`Ykj^>aiW%B}li1Yq*Hb~XUaM!e3TYXo(lK|IpJ6pKkglAZH>VlMiP z$6csv`Uat{J+L=-R506rD|$vvib}A!$bTE%QhlpthUZ?x&HA1JuBrCbw`y>ndo{Qr zRl-N$%BVmHAs=|sod2te!5hD=;+I6dTj9XZ;t!|)S4BlTaq4Duz@8HY%P^0GC+XeGQrb8k4f` z+3`9q&!(e3R$EN5o8KhiZ!igeBDCZQXvyAN%)70uo3@I5&LVubK|$}~Zj@EN&&#Xs zpjMDQZ{Mvdk9v52X$;zGdz0_py{UH;YH*C*}N*_q)?baDjY=cO$+W~ z3I4zI9raclw8Vd5a)O7xtC1R6u-W>ygPUW~kWU&+cw7qjl#k?VE$r8FO>II+i0auP zD|%amhQ~N7%*Zd$W-MXh^UwJrT&@Dz5@569gy#wFGNWRiN4RcRFXneN~onvUY-nv4z2>aZsQGlg}=7DyrSu)wXe z>X`CnI9=y~t-k0cfz&bBPSnlf8eo@7WQ?Exe?*DJO_)8k!fgGGv>#fWZN83oU%rZW z2s<5*aV-bj(@mnV7+6UGCW4vF+RCMY%(A&xlByb8IQC|@$K$+t3wd3;TLZgm& z;oa{1!?t?bOrmyO74*hmdc01^#*fG7P~pRWjRWs+1x$Rn^R0rXMO?f_Dux;Eue?STsO0S$Lzlw zhIbbcJjY3rOU<>+&Cq{@PER>!LSNLnxR@-RXSu!4`P9#%Pac3rXlF#LpDXI9!gVMC zqxZwmD`M_NubtcVAYFQ<*N9;Q0+2`INn4kWKwM;RJqE$e&|2aqNARvId&d1Yf9tQ? zX8jK`wxLJzT`{<6U}&A-w)Yi|Pv|Ls>~XkU0Zx1*q01=Q*9=YM`x-r}wGgn73}b3n zV7xD21pd$U=h#hsJkew&fw`(D9=&P2KBA7|Ml-*<+I@x=C%lxkz|^`mpWa6(JD{{? zPR~|ZZMO(^P#sNaj#gw^S%tGfe{rE|yhlGe+m$`Lkeil6Wxm(9`M8GYmAK^q-+xqT zV7P`cWorr4xzEdjG%0H=0L1^A9_2;!H(YW({06DQMz%` zyUC+Si*?Os+jjTHx9a>njuzwDIK+Q0*2(ytyavTDJfzt|t^{H{<2?4ie;Q>EeueZF zoftjMnJER7oWjFoTTyDn<7ZZaZo)!_si)h%t=Fs&6KW*8y=#SdBN+%?rFu6DkErwV zws?Dr2Zl^abWrYEnt`y5P-7$>OnCW49)Lr@H%1ynIC|oX3FzhD_Kdz`w%T-ex3bp` zNZjkqSA~hNur~@b0ln8Of9tJg?zB`zU9bO;{}XHZbt3)z@ad(KXdB5}Poc4l|14X? zn+)=nk^$|5Sm(rHmrI?S7Jq{L_I)HgAg>*hoE>uHKr_a(8aKB{W~- zn>zdZ$cQW)*DxpQO;mQnFfej?^Ru<7^M8ig`scz5{dM<_fX=pl4>j$#x{E(~#r^*Q zP)h>@mk(J58ix_)0k;w60?c`Tyku=i`ddBS=uQ9tnNtA(3;+NCYiVv|Y-MX@c`k5y z>|E<|+eViD?<&7T+p4TUiK0k~x>yy?mMklYH@2dYoN>0ej0yyr5-bql0H7|}ng`m~ z*eBcbo$dx2AVAUHnwkBvNErms=icYm4JaG0b{A=~I|-s)8QqKdQ+6AFM=xJ&h@Gt+ z;l+Ls-5iTWHr+YGlb0`EzL+NQOk7`27uh0_*ViJL&Eq5!4?%W&on~$(XEMqFxFPO> z$PeY6eCo`faDN)lpOWC_HWUB-U!vdbb$95rFD6eSjc4+DG4}~QZHtrnJRu^1C&fuP z6>j9)kA8BKxl9y?OL-rEq(K~s+aS&2MX%EpLhOj2nZF34X)LAzUHQ^Wp5_@L zw1s@k64%S5FIc0y!7QExzVwqBjb&a(Q|d9L6Q4%JCsHVriO6Cxb2IN&xPCmb^ds;- zgZ~gb5;*CSk^MwG73sqBWSUMFp-5zw1oGYu1&RI9^)%w1#pgzUW<*plv#CoePvb;< zCS~G>RJ@b#Et`Z^tBtRNhJdyGE!#A z&PP7^$fODZ>L}sA*RNl{d~vBfr!<=vr@NR?hKD0+(3dZNuCK{IWRxOAhhp=`ix1*{ zZxhe&$%y1O>N)AP32+_7=@W%tO#c$8BR$QMg_oHqpS#{&o95pBy#`!P;Y$jS)HgJh z@mwc^%XmT4O2&gB$hjzpAUUjYajHN*C+?gafmlkHILG7yZYBtCzF=Bp_;8Wlk}(6a zr5{kX2-%l^K*SAGgyo&2U`wPzb)zTasGv%2k%A6T$j4($%L2|KUMF%MQlz~W6G>*3 z;vor)6VAw}Ug)N&xN@iR*J!esPGur}$~A~MUrs3<22qe*UpuJ`r){l3O9SA*^N!YQ zsK+a#ED%_l@%%{vNSiALI6v>=(JGW}9))E~h1`ALRz|-xtY(vxZxmlLvw#JPu7^T-m5(y3F zSkyEuTrJ~D0dV$lN?*YwksU8)waL%4{|4!U3dlBH#_8kB7xK}Q^GqY>!FlyieNQ6EgFQbC=}ajkgE1(zhqd?}8Y>a{aB=$i>Jt(AaDiW&ZCTCFVrBB7RXWjvJ%R-{S#-k4P zh6^eFB)XM+yJ5OC9dYtedW+1ZriopD(NoM)(4l5IgY-_f&M^r1RYV>7n z$eTk^|MER#snBUc!SQ8#XtmnGFxc)Dx3gh?E7;yE%_LcFd!!a-j~rDH{&B1(^(?RV z0{uq{0PchHF2-s@Xl&+x&eeSB2ATV5JT>!1iJH0@EX6j}1Mme6L)lVQ<+x-qHK+{q6Gcyr zdEoo>G_o2doov>$9Ox?Rm5j&ywQZ4>crnpdT0xw5GH0jSwlyYytV4AcbWFs8DP6-gP?{e zLPN3BYaw+wjHYfYK|?DZ_iov_97oaE%Kf92yVSqX?+-t;#5Sa*2bVgDL*$Fo}67>pT0e9i=XAw`@j5XTC>tBS}`n8BdR~{y&Hrm zjBRWKVF9Ohra?F49cJG8ObZIU?Io-ucQHhvA0zZh8bQ(^BB@#9En&Oe$MKQ6D&9S< zY)ZozddIkbjG#O339%xgAzAGh+!w&AoY$P3gm={9qJ{o+RI>OX|fy`Q3@BpG+M&`W_-ZJ1#3~%}_Q-3GFfqL;)X+7%aarQQhy}OezxQV2% znAx1am0N)Yzj=(4E5~h5>Ztwp%caO3;(RNAYstAuTuK$(0=TW=5Wkbk_P}FhPY6f3 z<8c&vozp&cc%|+;)P5xn8d`35e#g}WSFH*+?d6-BoiCKjoyS&NXzE?S?qH%s*$DD))E36^|!`;_ zV9T?Mqmuh_Vd{*EV{)apmV>P_^LlVkc?0%XtIk=c(nyg%RzKTi4&xgAa5T6*%~Y>J zsKO`&q(xi?#b#P*La?3AQ99xv0Y z>~25LW;xh!kisgg^cJ^(no+EO9`a6#e4NYTLmGSwQY{STm}2Q-{FsH%#fDa^K2@>0 z*>6AV&NxU)1Vq80Rr3PHK34glMWva*S34MvHJuE{T4lm)KIdfX(xq>u>9z&CR=za3 zxISO5n#(Y`vF?Gws{4ArQTt1f=xe^`8d`CD$inzK`36*My@G5vr2tE+$N*eJ(GqTbEx zg7%z|E9FliqXJsp?6X-4M^Ppk7793YzRqbw&jLyrpqQFfxoaEa1dWnRcU-6>P+IL23*8A^Bay zs@cSVGCQm&{kLqsE4JPmf9Ag|MWKExvCgs%c`eSI?)Na&GV9vi9vtfw=QGnFe?%fd zsjewICPtT7kB)b+eN;2B(;K(O%dry07fSu6I604vfjanTP<=mqW4&x8z)ZZx32ipk)>-H}EclFOl!c(B6^Ge>cR+pr0@)s8NDO zDWCFJ&-TkNeC&=t5s2`LiDu6W3&G zJ-&D4LY?!emg%qDi8IvNFiichcVIaJ z-HFECD|~XM@2Nq!e|-OM=kHF=KGcLSLu>j9THiE);+tg7u@>moQ3G(1g-a=!N^Nn& z-eA5`sHI{I!z`^E{&W@}D3D=YyvXO5uqKDJOlF8M>tQKD)b&g1&4FWd`fe~BcDc-- zC!es#I~t5Tw3fK0aYw`K4qC1DX8!JHhL8UyQqU~*yY2pK#?cVnWB4gW&$gjNxLDxCAA1(vRu zE+S9&bM^J{f4O7BYHytLiCP{)4d?u>#q&~qeSw3jHDF%Ves*aN6=yR0S$SG*)1im+ zEYegJt|YcVz!UNnAMf3wYn{8CgE3lLD~x-hwEz#SB^+(ztwBq`4}UIZ&iG-9+(WBw zlU3$!LQgW8G^L;GYa`3goDT`{3=hy!TPikS>lYf;e=&hMvJ&{J7avPIxz_GHKYV^y zo;{(8mYc+-#bn|>!4=g4b3~yvc@l$hz2$YP#Z>7Cz0P7KUPZ8FTxv@$;qHZhEE{>$ zJvKp8LxF27NEEvXy|xa!)^P#1Q2^5VoZ3`5ryokY=MDR)SBnWZL26aj0CO8mv&;Bl zCD6yV2#X%xGA3*EqvxY8f>9H~!f34@xHU>A^#+ww=N$oZ$-1n5f63CYK zJIZd1zm6ncBWi)hWcP>hii#;z$t0erm;*B%Trbs{Aqn-)M_wWD$JQv8c_-Gq)EFi{vbJ7(JZs4MEI<}rV>=@h%U`tM ze^B4jg}hV%{e~b^(fe?wmVvx?7l#e7I70K?3$}R*#Ol{BI4WPfgciug&N@c{4{OnJ zUTsfHO)dIaYGv!1cSfGjzq=vYYWmq%KL_gPQ2o@b%5Xd;U*;V%hBBf{g<*VY%5_VUYqak)2M%67CgTm7{hX?#A}a=1&V%X68W z%}IubQLxh+n}-`%PsR(am|7z*qvB0t@EsP1c%9hv>W~@W81m()cRb!6@l|)sX1a{C z&oC#RHI#RY#zQsL8<%X)PB1lj*5DI6@%vC*(yWYvUCu*QF#ZEATEhQH9Pr{& zKszpYYbf`OV9=C0i&dIF2R@zNxF9~I)4rtB1Rw`{bn*^raumTI zg7H3}faycZ3E&SwZHi|S)I5N^>6IF$Z^>46Q&&w^wQGgf=4)1bPnqwKs`j7XbeY9N ztTsqy_;W-2&r_C?m2Lzm#ELmB=cu~4f7N|~?FJ1#YSYR)KH{dDKE#C4u{7}Y_AFa8 zZ=)^#RT2Nmst;Aju7(;{6|UoViGgBlaiPIF?AL7Kha1U3#MuTx4_SjaO z7IEqU?+w^@InB9^$iVACHO|@+R}wbvV_<^Y!)DGf>Jh68GxcC-&ie|!D>f?Mr6SFe_V=KY}lO(&=i79Ft9&qLgu>Qf%61%?y+m&*#OA< zFr7DTLiU*-q;r8XZs9R1D;**;|hAnRSg99!1)bRJ!;e$TDi=Mt1P=G z$h=|B5PRClnakz`hW5FhvP-eS*ge^t{l=&QgDy`o041A=*~3W&@W0++em%9Hx7XQ^lG>*a7(Y;M|AKeQ5E9M;i?`fVz^K2JTY8)MwAcsmrd{=p>S- zg?$vPy6hi3bD-L11sdbo1D+3Cx!kVsEJ2Gm9OIl$-ZXONT)}~ls{@$nh*J}g2aw%u zMCJ}^A0ke{MMCTY;2m-_f3o6@2Acrp5S;r!=>xjQ36LX{^GsvvmB8~M=OH@;=45B4S+aO;7&n~Ezu=li=VcsS;pRUOCX1G6e8p5Dl(Ji2gr{1NlCK;*msf{**x`cL; zW1>_pIns)?B7d-DQii_BCVr!Ald&nSw4I`yLTtiDsii?2Di013y$jmh)gX^?l0<3Z zaFe?gb8rATVnOBm2&LE19=#Ikpmi%!w1J(V+NaY(?xqY;fA3N>)^m;zO0;r?w5Fr` zqLnrk3XRET5N#?ViZ$h!R*vowZ8c@IA5;fa&~2(a_MV;}eozn6-i3VwT7`TPNe6+l zMjJ@2+zPf#M~w=aegM5ES`YdswBM~T!8i}5ouo{Fndy<`i1q(4McM-xJW^9c3&>_AYelsC^l({5R1PcM?w`;D-jB%x_vsR}8a07K82 zeqlRE?PF_XX|ibLdib+XuZ}_;^n1CU(PtIP1R&7nf6~jM(|jD{ZCX_t*_aLjS%Zv9 zr-4f!?V#Q!g@7VGKTe70<}24}Rki5-m-wJv)Ps?i5Lri2EmO+Shg8rnqq*_^R(20;V59wb=XPCz-wqW`LV zBuW+4e|Xp*vYrGTPtJ*zTYwMrrXTe`YxM~WNl$SBG47qZhl_Cpr@`eVvuPO(M>y+( zGd>w!Woo6Mhta&zTwp-ZR>*M1rnP9c_7p$p9^{CZ|+Ks5we~4C={6nCgwc8Y4zfIvs#lovI3vW1u zS5P=|Hc7bXW%rk7tPD-K;(pNj_;qnTDB?{99paKa@Tyy^$<-nE2+OKHyt1eV7x7i& z^aAmG)LFv53XBq8@F3G!IvV+|d~mhuHVajFq$3BtkEJaZwZv5KJ#a?+pa}2d!T94+ zfAm#UeS1%cd6v8Y9a|7Yxj1Nra{*t*tq5cJpJEtqt8_8!&ViA*jKRJN0f^CV@ITKc z+fPn+*a;%SPt@h1Z8HTM+fiG%aZ>LzV%@u+KFZsrzvJbK`?9THw%wN<-^&&6;V#u+ z+cenrHQ4brxbj&Ic4ju%_BGh?HMlZwe}gNg!Iili^pdo5V}qql*6<>I4kzV)gF-a* zZ7R&-M>q-cou=Jx%<|MSf~|gXHX0Bl`67wO(N+AZiW9Fg=o8Ar(P)^ZxYc0YiXo*! z1FLLQ{hmT0zSW*@5as<7-#E(qUEff0edMOmEDK1vlGFHJpz$Yx#&_p!yyG;!e;a6g zGtl^6pz$a3HooFC{v^=&ZlLkaK;wHiHh!-{bQcQ%Oyfr6ja2kf-39!_a7BqaL`8x7 z_G!KI_j6BooDg$Xlxf3V*{(_zPEo|spY8sA#T1hqoS8z4dmP<(ke+g@h@O2~p=&a#4o^s+loKli$Q`Fwvz`3C{{I-dA zkL0%&PZq(WUXibz4$dP*fyIY*i||%a4%O7DBmv)E6#(tdCZGWFBece&O%Z^iT4!|w ztpT+Te%lPS(II|-40!Dgptj22P#56aEu3YP8pYS_?goe^4&8CeUV) zRdgSnmOcP7&~2{)DE~woZD7BONyG|zzK*`hJXZRP#__ei8dzUygZAy!6_g|PtV{ez z65tGyMEDwQc2JIF;;}Ry@M60h#=xAn-9ohvC7qJjQ~Rwdi4!Si;He=yRU7@l>|V1rOd7tm^rtK+LKpPRr z)J8udHiX~WYiMYVT_DwX)()h@sk`v9`Vn>m`8+1T-so+4v<_S%LDs1S(#`6fAJRRLUK}E*6@QI z29|HHhj0J%j~Mf%~xD&B#MqfH*l1QAy$m&7!% zXcdv0INYlwJ1I^Lf+jgeki0e+uH7MF(N!p(HZ=o8w?kdL9$2?+lT{Q?Qiv&t6gALg zjmnY=nIr*FDlMgG$tnQP2y41ye>4vq(hdm^ji5TFlx|Cp ze<#;xYXQz8zbgg1Mt1FJPE`_2Jr5=&!pd?*RKyq{>Kdx87F9q%LZ}vKmXulr6%@x% z?9|v$S*;sxYa8;A4KdbA+(F+kXDL~ZNO;;R>JBdcr3!c-}1fkkwhaHXA~QfSO0YNyp)5rb+vt zOHfzo6>2P_6_wF%8xx-Ec_pn>Zz0XAG$1uOwS+Q8f1nV@cY`{Zm>QFz{4Xk!HDlM5 z+6qegCI=K)QOY1zR8h*Sq5j?^resm70Asn71>pydN9cqIjcS`%kq|9%B25)&Pc=xR zPlHG82^~z=kS49oB2;Rfz@D|T-n0x#nF0Vw%t2{Uqk`dRMhHr2S*m!75KRF(7}|L3 zC^rUle_645589n(UC4$X=n*X@?Vr`RG`Y$G!(MelyM%Q4Xn2(RW%ucxkVUDClw~D_ zP>vRpE-Bq@<-ek#cAy{hGU*yBLra%bIn?A+{6n@B_u35``wI`#T}MyT&e1 z`4Tb^U!lRnpTJqUkW`lR3aJWcv^I^Er^g{Jf9XqnGlMoDboX8!nP584(NCV}eYEI?Vv@-WOr!xBF!ZC>)Bb;x~r879{GXN#7jPizraAc&hy% z9YwZ?DnC`nh4-{wB<(eB_X`G~gDdr&wRM)NAXj{GAAuf1x2&F<5Y{Ayt>K1I@ZYSG zf0;8kvP|#-oYZfxqgT$0&T8q!N+%fQOhu3>1I_{p;c6U@G6o%UuBqEdOQk4$`m4HDf4*}X3sI0!P9;P&g$md*2+mmeS^jY!o#NoF z{<3Jo0P0{Gh{%Fo4V$ViuPs!iRp@Y2coYaM$fd(8v$q8r*}iI#)gMjg>QZ@$U91kC zC1uuBDy$URz5e`ziu7Q^Lo){@V}w8orIrp9;*k`LR5o<#cH>M1C4{8j zOPb4M;ex~M)kHU^Xnv_8s4s#yF@@+kff&b5B?n=82#alM!i>cQ226FsGwyAJ9=i0F z59fJ=tprGYd=S-;cOw)iBDX6Z0QO^`ve9bZ*3V`Jz41k$xsf!@eB9iqG>m?e(@zWE ztJm)U*mp&UV8{92TUvCBAurUff5Gz)CdLPIMfav-WS<^<0;_sLsm%8*Mo3ya6c)mb z9o$~-Bf;@xJdAXFbr$i_ozbhs_MzAYMC@g$>xurZt1oGUP z746s=6Nuw-G}P>qxw-~-e}M-B9*sgV@zIiNq+?GfqB#88;R?CWEFS5lv8gtcP~W4m zLjEVIR}DSA6%HfJ8YxnerkB+cGa{wx&_#l|q8t3u56WOO+Y!?gjm0Hp3V5W9QD}L6 zQN8cua#LZ9v|+Yvc@Jl@nFp`tbYhFa7R+bA1eAzvS=z{jAq6!!{a0h6)a$@WI)2j^T9Xq zH4YGwLtAf5MAWXgOu(0x%3I+@*KjEm^~Sct5ba0k`_az3BSq;ggF?7&PSPNh2pu1d z6UK(efE`Eaq~^>*fBewN+`bPHgA0rxxUtEcbeUq`JS4<;v_RM}^64dG4iw20Ej1t} zg_vzRj$5p!X&%Ng&t2#U);4CHp^O=#;|7#P7sAcnhB8}5hE)=R?e zn`iY#W^!AFCEl%DF7QgTmGw?6i*wD>SwygKmTXfYWmZ|>)axr{N*NLjyFVqpNg27h zLh7@|DvK&CJ6-SOI?X2Bm>7ynlw|{C#l1qv7xBcWNH^=`S}jKjzsM#@+%E%v z^WDz=f7;QTC*SQp?HoOOxxI6=|7^Q0oqt;I9Wirq&0nO}D1(~y9JtAur-LE(7`(5l zzoYnwFJ7z8*w7lv!#Pb?p{x?CgW?>cK8c@`uw-e@y#Bx{^H=?dL_BS4# zQYv0Tm|>OkafY2#yErz*Y<)quvGiEGgIUy1f00UhA>dY*;^Ng!0?BKYJ@S+G*RLB9 zmY$=Z(~&wwT9pYrB-8k~9-S(Q2=?;yT*myg*&B{N`7(#7B2ASAxvoLY?0}S3iQTIU ze0cKlJ9-E ze;^pJ6pxG2`%uGpw4XrkdpD!3CgwY@({LJN%;M~HkanB$mnWr<8iFG_WLv0FUurt< zsudQaflBeALn#iY6J%{6Ik~|ker|v2+mb-2NhNU6!)Rt3Drz?hepNtlM*gcoAY~D9 zlR&LDFOsXe)d9+B0DU}U1?hc`TxrnTe`0aMgZ+-_k^KBCKYi_*p*)bugX+FLLJnbg ze<75ehyi#eS~?l;iiG7Z>~6`JsgB|;ypU7YfH^+u7n6!mETqJ_qM6rb!Dw5#g2w;ZQ7G zti2hp?@u1RzufrS_2v&hZ+BZe{r28*=iAej|2kWJxlpLGjw)`cyt*nbe?Y!1R@p$6 zA9QaFw$iIlJM3%nq0&8e#O>qt?m|A^$f`0vj(a+$-rr}{=-O|+Hp&ZL4%7b8$OPOp ztsUW3=$qHiU%lIVTQ@J4fASe1RLi%?WdDi0n6ZuPNAaj&S7Dr8lpfBIhV_Vuwem>+PN zC)sC2M-}wAJf5n=bJ0NKb83|Gh*k^SP(M7Xr2!7Gvi~uRYs8@P;SX;vF!Uqw5EwSunc3~T=x2jxO#glCsH>V}8@~3ffTo*V? z=B4|y4AKOE7vEE=e<+@<+COh+E#Fo@DH8Ztv&6m6cXbSOmkx>sK>()M;%E#a)!t(T zH&SIIRoI08=CO_4$5HJ8=uzpGymaw7NhUAiY{Ij4Wg)U3O0r|$`%Al$Qe2fOl$yzfBjl8+^z@{H=}ECqC#Cv@QF@;x<5w|Gj1-At-b10pfBnmNlq#aw_x{o$^-+@9 zkAd1Z%B2@wJU7^bPDwWOL`e%{SnXe(rURAD)Wy@<3nh>M<~>xFjK{scX9UXSWTK>t zZsA;cU@n65PuTCdNXlzI(@``z9S^4`r|NUNxzg+e62EG$S=b(k({mxDGQILlm2M!a zL$>Jas7zu!f1r#~JkgeGT#F?a4N};6S1IsR7EIlq@~(Mul()OBTW*SF=t=o(l*eLY z+e_fpYtzZ`a(jJwkchs1{MenIe_N70WD=*F6H8(0@o*%QyW>H8 zQprA|N_BIVRAKK8N24J1eG7W6`ag z8C!hWe?8xdsaY*OW}VX3X71= zIE9J>n%R`cP?;k_^u|=>dC7_>5C}M&fSXrXf8ZE2U7yH<-QU=d&j|%j`cB4Zsu!Im zk)yn2zrC5(4qLUaU1#r3hzhhuL!`2gfPK z6-RZJ63~l{}jawM&Zo9j*3Zq7Ncw^Bf7rIl#%CR`>+-f^@kMoQ>0#3 zfBV*mts`L>x$VwUsVE{q3snWVQe?zZkAX$C7@Rf_#S7jBWH{Z++u2@jPX~0TM9E+gRINCo=gjJso2Yp-<$xg+e znn3zTGF46b5P6(>EEP+!b(Ca@YozhWxO;UuNyV+mY=WO%;W5v76d4e;4y*^~fAuK2 zz`1Sx=dr6Q@4BwAZ3zF0$Pc3J(FmHI)A=f# zTMeMoux~p8kDt=!`5g&42Mx`0e_ymT&#h@{o*QVJ(BTlfe8TD13}D#aD1N=UuBwy2 zKh{}2pT-0C+$KbfXadg?7%p{0_1X?Ew15W3slKCd%z=Osm3gSle30B}I6R|vKTD0( zXuA5vs~hZg1fVnju6XCo=-n`SnqEhBb(iL%CJw>L1ya^agPbYhd<^Ube~uhr%CzCZ zT&!}I#ADc|lM-TpQ{!%}45*oU#aS;+Yr-6=ty*O@bg&l}BmdR!lZD5m& z#$S)RB$)%J5%Q+E^)Gb^eKbm2GH}+kE z(TiD6S;+M0K~(6(V^YdCe^MrwORX~a+9dJk z1^IhZdwsdx+=y&vO2oc@ny&peG-U|iH#>hQ(yskRWOb-I9H%EK?qbwM(F{^t(>RUo z9;DEEso<8=A-_9qf7yPyef4t|zlq}?w|_qW@#i1^`t!+;JKJYJf39r*w0*Sw!>@;j zs<|eh{=W2f8op$5bLTO!U;8Ol{1oVtOvN`vh0KpK@#r9`4wv z$}$x@JUTTV8)-ENHmq7r(5U$<{ z{G~kSWVqc!mBeH*&7`A9JT5qe)$Q^HSwg!IV7i- zGaAnu)`L)~An&rj`tFkh-4McxdR7mM@MZS4P+Uf1!caHX(ldA~hcc({P6MV5?Wf8IPxkpY_br+dS3KP!XC@a24{ zSDz-*@_>1%6%?*E3WOxiKH+c79mt$sS;apGkC)q}iJ?!V9@hMxSHAZJbSOX?@krEl zI^gOZVmauWJLLf9YKxCs0%&z9`i2XZ)xpHwJT-k%_ko>B%?}M4c zTj_9Xe=9Rfjq0sx0Po`HMEW`M0nrhCUhb~(oTy!_Og<@DR#M6^7UpPJ6^s@FjkP(q z7s6S%-`3FU%4vQ^PA%Zt{f}e4=ot4V(|F(mNx69AEVN^jL#>vN6TLj8SI@DE$R%{V zs=~_mTyT87u5p|}ePL!1o_@ZcSRg#F_bQ3kf0ymT6kG)L2|^UH;(C_GM1C;5QUid` znqanPfhY`%2BZ9(u3>FbLV@+wgX^OLGmHozOs;I)!(BI{)Hw7jtB*e#wYKKljZszL z6|#DOxUjy&1k)9jOBfYR7lz6d`-df-c=3-)C$G$t^amMGI`FJ8*#p1V>clTG4wwb6 zf7tp8vO(ifqqvs_b6MSaEpFJ_uFYv}=e4u}D|#@L9k|D;+qq7&y9tf@>^9X!$i8XVI2IO_h z<46`?Oe1_yu7=|?6jU2CgHH_;bYz@}fBb8xD4^QPrrxkPi@xng-o6|EnDvy7*)@BuwTUqmTw)!|HY1LISuf3w$v z^h8A)Q+jT;B^2st|72l7UC*Jod%A*ts$=wOU!V0ne_^aEA7A?^rN>x_EBT$_IF$3|dKyu_*I#PP>+$QR zXPL%Zd;5tw@8Zs8y{q{2^DmMs2wuJdu4brze20ILsSBu9anA@BgmYhuP7>XPZqkbZ z0Vs)n5#}s|(fk+pp}&l?*A;KESWt5d=1mVidzwKh$#m4Wi@sH#`geVqe_Y+q@!+C9 z;QdVBBE)ui^@hD@QL5@*ewlg0=tJogFdn5^q%w}xk+`EY=_S_59G_PyUW71;C)g>M z#>ZF+J0D?jZCtC>4iEGK*yH+P|H0DX$HRjsST)N3+&Rmp=a1_Le?R>AQN8(gn#%Ws z!(R@6jhBCJEpHw!^NWCMe*pgTZ(qHBySKBwzvo$F;R1^k!uk$wC)G7-jhsY2-;aGe z%BK$JP0qFIoh9KO^T+B^zFKLXFH=wrxJb!A{+!n0`_DJ04U_s`a87H8;RE^m&jtVc=h5Z*f4JFcLG3!TRD0&(9;T~S zr+Vn_%p2}HSv8LippyI9--Mz(6er2f4g7LJbq~-e-bVGZJ|5lf?vtf&%2%frwVhX| z0F+CZUYHyF6$eM~f2x^li#4G3t$U5JTGreFGG*O6L2g;qi|N9yjXMS#tmot40@FxT z`=#|b`?W^pYVRA>ere<7LQ~xgtrxIr6dSe&$4-;$!f7$jl7c~N#~lCDbRlNVg>rwR z;#R&sg0cJL-~Wv}lxa51u5}CMf`XkW>KECxtEWKusgJ@5e{`MRaIEj$M|lXO8cx-B ziQiCFWpFJ-a6M9QkL>rt@p`H^by2{ev9tT)g}%39uO=36JLUI@dF`opq@olZ7HMK5 z0JwpSJhy`y@cYHB&W6&Sy{4jW|9BB?PBClL*kUS+zK!E?Vai~;D+A+}sn^tN_b32l z#1@yyktYnPf9HzMbRn-~4+pu2f?X6q)Vh69Fl&p^Q}hi7m-qKy$Vlo+hzh3WQBGO6 z8iIlBz1n&E!<%<}(=LD9-OE3=${$~DfB))j)Lx}&X3C7Ea*-!5UhjN&wEz6aJtXz; zmUF9Q9B|;xC@f-}e{ny{Grh>fDhmsP8=`R^?NR*SJpk9nNp7uc(XUEe+F1^tDu7 z3NYo%BaVQ}{IDf76wP#}qH5Aou(74l=7UNp+Dq?tyE zyiutxe@938-lm^!?vY8lbv=|wN%4Vri|8Rx1))hn;Q^vUzYkn>1Ic2v{xIBOkvqgxE zr>?ZsN}pSm&bIb(w#wa6DmS?tJshox z#T-pKO((8$MP2x#0csgkLV?Q<4Hm|4B25*k^E_T5*3qN{5`F_D{(%MDd0=WEk@c#& zf1oI+_ZA2-^xR7b#In@CNV{qPKR?i@3h}1mAGAV+gqpnxrb;&kBrRlh#`BrMMqTs< zPAJf7dGTrLxa;_yB8{fCiYWUlYDZ5Ip5u)FUho&xHLU=K|m>uo0`pwe0M0TU) zJ>cdEw|b6HT9+;XAAfmpf6&%O z7g7y)ip^kyvvR~j^Kd`{rqA@m7(3mi++sO>FR}2 zw`^$AoY&A7AM4LK1JxM{U6v`2UGeT@s5c=b+yDx524CzBhi6&yPJtF->}xj#DXbN$ zAU0#|N5)a?wRK#?4=YcPbwt7Uf75TbS5JOkes`WA8X;p3UVgu0!w<&)K+M5f-pQx> zo+Oe~jE4wnGdzyITYVY#cp>k5T)O1lF;n5SzV2jpz<|%^26n8u*q~{dvwh;CdBn3I z%h(#S1i(svfhNGge9e1?^-RWxe93E@->dgd;KthL25xl7$ipl#R+Y@of4aMb`V0xz zcE>VxD}@eIYi{XqoSYa)V;#05Qw7!PKbPyVIl(0=EXJNPhZ{lir*pCBTNBePbAy@{ zBRwEa(e%6==0Uxcm&2?nxF;8v_1eoH_V?aC-+uAx{mUnNZ%ZpVy278m*<^GZW*<=f z*@XHV)m=-~JMRy0)e3hCf2P^^Vb)DA9+D=}@>SG|mdDX@4|lf*$@a6I=euckMp}+* zJ{bBpv-+5QVpn({jKT8EhGGE)%qH^{!R8y@3(}Af);Adpws0{HYi3ee=LnGBMSX6$ z6`#M^ct%68fpFl{Poi1!HRR?ZQ40+Fwse^N5lR2#V{ZX^9& zxMQOBo?(nsVdqmf>+3zkqK3AEUVP0Jw&$FE7H^Pao98EI$Jn!BjJDNiC@c);Ner$f@;k*;U2U+%em?CYzcs^|leh!<)p)ywNO zB`fE+4fsVEZX*nRQ6f<>wbpOS@aeH-cXfgfhh^%uhyCQT5}gxIT|VH*p?KN6AM1#V z?kcj~_^#OXQ7yvG8vz`w`F05@e}t^1ukHgiQ`qpFYi{Mge+Q38Bd@68q9dDfdVU_d zhKxrKPQQ5OQcEH}&TDtyzkj}KcZNqvFFj6^z5)-}sR~(Gw!vDsVQ1c$Qh=1zQ4&0Lt>_?H-v44t?@@h(Y-v95a~ji&gh;eX zAk^DsXOjtRe`j8Iely*a-$f2cJyED8MyP=J^#%~j6%byR01|T-Paq!$*A-~_U4gu? z{>``nfud>@@Q+v51PNVWHF(-->+t)O{SL_mi6!e-Uo3>`IJzW{s5naPK39*cQ4) zALJWw4(Nu$XOH$VPe^MCQo1{{ERV4{c@YyL52Z?;@yqRHy4nK`LZYv_8u@;LW=4*9?xBYm73M>2IJ zAmLy}r1VwoenLJc`mQ>oaO*l=Q%B0Lq$0i~DmHf(t?>P-w{L3rP2Xlj?=b7!s*Yd1 zUqB|;XDH2yk1gCMa6sA~R9B4S&Ay07A$O4Ce;^zVV%A#M2ux&O`}Zo&^$Gdbb*m*M zX8iQ=2OzI~R7BLj|DB@|D7)&EUAdv`ic@y=hO!-}Z0CluZKrJehO#ZEY->hYBPX@5 zTFuUN@JB8BDynP14TDX?bfIDT-7v&z7z%p^?j}22;-;XfyUAu~Tc?Y`+Np1P3Mz{A ze_E`^rI~$Bbs+diCqcw4R82I+VDu(~?x*~gz^sG<7EZw^BDSU{mX#@7vc=|vW^o1B z1c>Ic@@SZ4X;&1E%$IfHyrkN&s;m{Ox6R~w0)ho>{?e=p{#EMIDve&wn0K{pR(rE% z^qz{E6>6LIacQm(1ZQPx6+(a)7{#Vae?UC2=uMVP`$O(_=!=Qq5tH!=0j(y>>4iBg zqmQ9FJjd%&+DshlZ4(^;D~tZ3qA9B4>f8na70*;x4{%ftZ#3+I;Ih8WV1?~z;RjLe z&;PjyMLX;8v719qyHfklwmWO<^=!0c?o^czbeoKF?e#~iE2S8B+|%xjfl7WZf2HgN zOmxnHX{k0Ei@||oh<}Jd?#9(JSCyw zES1l}70)Wzlk~fuLkpE;v}n=PFV+lK)QA4J>QgjtiZO3M;w(I^?~%YyxqnbZO%&dpl8 zv$@gkBZZ3m-^TymqN3c?iHOU!RM;>O13bgQazzBz9`XuE+twX+gakH z)4xE2Hp?+!i!p_jP}r!(r8ysO`l%@SGHdOLC^9QYtgfLiL? zVH9kUO5RL2`>DNXG>CgPf8j8mJf!lLJ7EkYB)IuZ#G()^>8SwRrGWdq)-M)iFjS}O zqPk2DWd@6zmPPI0z{3$Ry>VSWW$|!F#0~G4Pjds4Gjd_4}h(wW;5ob{d8IAzekO8Jex$AI+RLes^M(`*-gI zGxX{-J9np;N<{FlNw`+jO~e^%NA=F5PJ4AugK;AjjRE=}L(O!a^bD2O9%^mz*(n2#^k-yKoz~s2e%3uTovs#03&|%ALdhr0bDJa-=IKqLJ3QMC zb{_4o=L>~-5K1P&UE#^>WESR0=SXT{p0hh9w{Ys~ZM&Kdf8|x1U18opW$u&SOVSJT zoXr`)%zb7g!7xu+2yAAlF$Yp-)gWiHiWegU?!oai@_(}A)N&w5V&>@Qr%m?f7%@G{RXbh;odEPZC-!=K(^+& zOF&!m%oW(yJa-Rpn?tyDpj-3I3V3Ut+XTKf&)flU%`;yhT=U$@X}qn&7^a&vf9r)2&} zxjB?~f2a4C1ty0&a3kkN%aJ#B?tu3Kk*QULCi#aLL>1~~j%d1hOa7?3HAdB~-7>0f z4)u=Fb#Jy>i46o>tIdvhYo1W#+ga$}J`+q{_cNma=YX``oy;8#*gT&b!`D3X$MQ8# za+{dG!AjxSzUC>#__gvjxm6Y#^Mt0@$<5hlf6VjQ)9brTt%K8Qf3-9m#@Bsa@=iy9Z1hgUq;yA*^%&KgX{LG%V^!I=(P>#wR=Cr?8&v)EmywzE3! zB%jd}Fgxm8ltoBk&_RdZh4RO;u`a3R1G7YW&g3@c330QVqXYUUjusNe$5O5GeG`?8Spue^{m zy%?l2m!&jnB-tGFwpk?+W;VyRaedEsf6T84G_X!mP4F`hh#PYP<(1HZFH0?7mRi0n zwNxL^Qj1M|@vpJeVx#Q+lP^zw}gHteZW3@SXMfm^0|y61FErgSM2Q)!4Rf4Qan z2hZkBEA`u^bF#4KJdx>*DgF-YBUx{p3JTnP9#1bYna0$`TsqU2$X@S9-#6WG;~EHL)*Py^1^4uFv%>{rhymQbrHGLO$R4EuCGJ>!VOFOmCc!jD+ z4U}=vR<2%!J4Mf4CZg%#Q)nI)fBkftTsPDmA8D)?XGybI54adF5EpF2%G2D2zz}YVsTyA)tJJ6?Lr{#IJS6^BWr)jcuw+LZx&iOYf2vI%oX$^q z_tUXyraT>z*TjusnNXnknbT<=64wPAQSC~dm6ew9Lf-qwNPlczjb)Nl|0x?@H2c%@ zQAU&xS|7{W!pQP|e2D|qq^_{cFRfp#+R-@7)(KLNhtQ%*t`E&!nzMY9E47k^W9R{W zZh==r6Pm}Ns2%`%YG#Bwu1(|FTVnw(#R&)X*S6$S#D>8@Z5EH z@!T-PzAoHph_#I7cLVebeZ5b~(lm=@zAs+u2{eJBglX@oP@IxUtf{bOkvefOZ#!rB$}4qgKUpWFY!B_faqZP z^rQSZi~gd|cVhG88+r8?&CTC@v;${pk0DWm(i@~lSh%xSLlMqShn?GxkCXQoDmO*a zujgNu3Pll#&RCsaTBd!EuEejU#gx;poUt zyNW06ha)7K6MVFpmcN_oABAN5@yABAaC-LGI14&cg)aInfB(_>DugHwWYa5t1Ro5^ z%XC2m^pv;c9~Z<=03e-MmXiigNxJe67zwJvTVF$wkZ zX*vINu*E{F?owS2?5qsTq7;8LSZeEuuEKh@of^04Yypfk?!sB502-~3OFso06MrMW z4l7K}^)EdJe}(Q9ZF=0z0yaH|VoFO{{AEL)bJWRZ8-u zN-wHK-)uTo=b}wbwXdSeRH2F08Ys;5iwj}XLRZ|O0D-(tRUs;eJ7r)Mu_xm+xh!Ag zh5-;zMrCTN)5_J`$+V>2igK+}nZ7%tPw9Ejv~Y1@e`#NTo1DE&GO*#3;dS-nSlm-= z%)8paY1iKFH`o~psrVFjy0V+bgW*YCqy5GDJP{T)+b?6R9= zPp`GY7-le*D6;P~R8owoY`18J=(SCvJ8Ivb~zJ8l>ZA+Fa3#$}FxYM(POv@x);~O|)~&OUC4)%;VOH zMhtO;9~XYxHT8%4Q0|E=JUIuaLW=9xm!#KA7;Enp`$&IUEoom6p~l)y{OsdrE-=~_ zqy$r_Dk&Q=*byr0u~X|Xcg*3=uzALI+1en~!%mD|+Z@vvwHu{V>KdaU^}+I{@bBcM z&%vh`Q@D&YZcGKQ;zJFOO<^{kt!7;~o8=q*lBHK*Ut8g7)muPiBpfiaFY67<=90$m zZu`4kDwd{R;gSAAod{@IS!|u?j($gvEsKo|8x8za^HJ7`JNi?3sZWI7)NT<-r#}jaG)0Gmd2KA!)}sygVaO48A5b&%q>4~4!F#VBu*dU@&?g8J`1T=w zb#&lwA!(Cy;ZTsWrBb$;XH{%YTJ2PuY%|AC~b^mYowQ2fn<1P80*G=vXXTHlzHK2&;p0vHq zMV+pyy`nTJ2)xXQSWjgk1&;xlCqgmoOCOg`oS2UF{F9bPY*>4idHYEoW%j~YJe{f2lYvpCkDk8Ix zdN$}rS(Rg(PB_)$#)=rrC~O~cHARbBa||UsbM5LYT`}e8hO9qWBiPIoVO+Y2OVlZr zZx+U|7W>*$cukn5OKhKV79Un@muV$W_i(o8t%ZC2?tM5EMn>nHH!Q0T=0YsBKeh!-AvNmA?G)zWhI z3x)6d##ww)EaYaX%e6XNujRmzNkVM4t#6Vi~&sovP^2$CF(f>>HzM>O3+OUQ#ts_3+*2Qv1rTVXl?TG;}Ju zBJB~)Dbv^CXa0fLrJBCIrHS_JyqfY9rHE&^u@k9yyBa$fpA_3K5^yDs`by)rGwL3B zZvN<~=Cuh%{G%V6o*Ykc-D(`|a`aV!R67B6369mF9pr%6<#BE|62q;aAkt(r;;oiR za1!SU;$>YR-y&Yliq%dR`JI7&)ZCD79b)GH+}o(ZU7|PjfmLdv=CRU{kIJu0HniQ} zUaX?8({MX^wp;}C!-y;!>NnD)2mfy!*D9E2miEbFC(?~8N3n{xf}E<8)dObnBTM>u zRXx7)gsMlAK?lat=yhmmUG6^>c7CQOC;80eb~wYQyP%k`1q zuFWF~=B$9_YAxreaSd`!Mw=*HF|&w~qi#?1b~e}Z>d#HYZ7uI4 zPkonut)fhQw7FW6AG@=2rR4R&o6LHarC?7`%SzdKcIR`^RL7EstyJ}}h}h=6(x$k8 z{Fpsja;O8=gsT#hH{3HU38{yuH)eUTEBN_U?q#fPz@;=a&M`f`j)Ye2)7 znyXyi#_b%TKz}^ed0hQ_r)ugX2G@SbmC0mCO)RB9VejPUxx9!EMdXOH^Szh7xWLNh zSy!qtiXI<%EOeH7gixE7ca%A$`D~Cpn=tw%0XLn}UArA9lV$L-xa>#Ta^1tu#yc2v zIL^xAu_Aj#aceP}#+A`6ewdtlu2MZ>=m*1O&vGkx>mp5SAT^}i<7@BAn4z!m_V`wj zP+vu&AFFUi%dvA(dow$6q+-BRgUB(a^og?P>$^EpbnJ_1eZ#h)D0OxZ*CCGB<>}a~ z7bGN))4%m!KWJ3M$KWN(4(gg0WrvsJtG$w>j;U;o=T2)AU>a2wrprUKBW|qx)*4T8 zehs=lzlN=<=wJ;k{iIE#$M(1g0-p>!9 z;5Q%GA;9aF%>qqlNDrO1hs^qhQUFWN5HEND!+cTI5P0qk*`i{(!671a7B$5O0*R10 z=TT7qw12C%M>Hfq@rkbYTiGRSs?=kG;$N;7N9vF}z9TeLJh#GjgGORZ^3l_-GA!S9g<^{>0v_vYYHR^R6XG9sC^5pJm54v-hmq)a5{IUL8k|F9IZ3Y2KwjN1i>K+ zq=2&j49qBS5^BpJkpioCet>2QtiHPoe!~6%egOy(9_Huo7v|>;>?x2ScqRbx{(>w2 x>#yR4L69eWCZ-rD_k;{&EA1dej9-wCUld&2+5HZ&|B~v!qlz1Wct(I=?;k^(bx8mK diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py index 080c194..7908e6b 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py @@ -49,6 +49,8 @@ __docformat__ = 'restructuredtext en' # 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 @@ Decrypt DRMed ebooks. """ 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 @@ class SafeUnbuffered: 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 79b17f2..3a56e44 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/config.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/config.py @@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): @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 @@ class AddBandNKeyDialog(QDialog): 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 @@ class AddBandNKeyDialog(QDialog): 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 c5159cc..493f950 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 @@ 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() + # 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('/') - 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('/') + # 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 + # 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) + 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] + # 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 raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(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 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) + # 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) - # 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) + # 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 - # loop through the item records until all are processed - while len(items) > 0: + # 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 - # 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) - 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: + 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 + names.append(dpart) + return names - # uses a sub process to get the UUID of the specified disk partition using ioreg - def GetDiskPartitionUUIDs(diskpart): + # 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) + 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) - # loop through the item records until all are processed - while len(items) > 0: + # 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) - # get the first item record + 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) + + # 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' + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash - # 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 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 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) + # 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) - # 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) + # move first offsets chars to end to align for decode by testMap8 + encdata = ''.join(edlst) + contlen = len(encdata) - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash + # 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 - # 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. + # 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 - # 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: - - # 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' - - # 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 + 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 c5159cc..493f950 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() + # 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('/') - 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('/') + # 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 + # 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) + 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] + # 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 raw keyhash string is used to create entropy for the actual - # CryptProtectData Blob that represents that keys contents - entropy = SHA1(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 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) + # 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) - # 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) + # 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 - # loop through the item records until all are processed - while len(items) > 0: + # 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 - # 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) - 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: + 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 + names.append(dpart) + return names - # uses a sub process to get the UUID of the specified disk partition using ioreg - def GetDiskPartitionUUIDs(diskpart): + # 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) + 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) - # loop through the item records until all are processed - while len(items) > 0: + # 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) - # get the first item record + 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) + + # 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' + keyname = 'unknown' + for name in names: + if encodeHash(name,testMap8) == keyhash: + keyname = name + break + if keyname == 'unknown': + keyname = keyhash - # 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 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 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) + # 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) - # 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) + # move first offsets chars to end to align for decode by testMap8 + encdata = ''.join(edlst) + contlen = len(encdata) - keyname = 'unknown' - for name in names: - if encodeHash(name,charMap5) == keyhash: - keyname = name - break - if keyname == 'unknown': - keyname = keyhash + # 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 - # 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. + # 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 - # 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: - - # 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' - - # 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 + 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):