Fixes for B&N key generation and Macs with bonded ethernet ports

This commit is contained in:
Apprentice Harper 2016-04-25 17:49:06 +01:00
parent eaa7a1afed
commit 3a931dfc90
12 changed files with 973 additions and 1944 deletions

View file

@ -48,6 +48,9 @@ __docformat__ = 'restructuredtext en'
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 # 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.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks. # 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_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]) 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. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@ -87,8 +90,12 @@ class SafeUnbuffered:
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,unicode):
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)

View file

@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label) data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10) 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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept) self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject) self.button_box.rejected.connect(self.reject)
@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property @property
def key_value(self): def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key return unicode(self.key_display.text()).strip()
return fetch_bandn_key(self.user_name,self.cc_number)
@property @property
def user_name(self): def user_name(self):
@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self): def cc_number(self):
return unicode(self.cc_ledit.text()).strip() 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): 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(): 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: if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!" errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) 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) QDialog.accept(self)
class AddEReaderDialog(QDialog): class AddEReaderDialog(QDialog):

View file

@ -4,7 +4,7 @@
from __future__ import with_statement from __future__ import with_statement
# kindlekey.py # 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: # Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 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.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode 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.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' __license__ = 'GPL v3'
__version__ = '1.9' __version__ = '2.2'
import sys, os, re import sys, os, re
from struct import pack, unpack, unpack_from 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 # or the python interface to the 32 vs 64 bit registry is broken
path = "" path = ""
if 'LOCALAPPDATA' in os.environ.keys(): 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%") path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative. # this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA') # path = getEnvironmentVariable('LOCALAPPDATA')
@ -994,192 +997,113 @@ if iswindows:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
DB = {} DB = {}
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
hdr = infoReader.read(1)
data = infoReader.read() 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 : # starts with an encoded and encrypted header blob
# older style kindle-info file headerblob = items.pop(0)
items = data.split('{') encryptedValue = decode(headerblob, testMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '': #print "header cleartext:",cleartext
keyhash, rawdata = item.split(':') # now extract the pieces that form the added entropy
keyname = "unknown" pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for name in names: for m in re.finditer(pattern, cleartext):
if encodeHash(name,charMap2) == keyhash: added_entropy = m.group(2) + m.group(4)
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
elif hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group # key names now use the new testMap8 encoding
# is the MD5 hash of the key name encoded by charMap5 keyname = "unknown"
keyhash = item[0:32] 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 # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
entropy = SHA1(keyhash) # 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 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents)-largest prime number <= int(len(content)/3)
# of the number of records that follow # (in other words split "about" 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value # by moving noffset chars from the start of the
edlst = [] # string to the end of the string
for i in xrange(rcnt): encdata = "".join(edlst)
item = items.pop(0) #print "encrypted data:",encdata
edlst.append(item) contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
keyname = "unknown" pfx = encdata[0:noffset]
for name in names: encdata = encdata[noffset:]
if encodeHash(name,charMap5) == keyhash: encdata = encdata + pfx
keyname = name #print "rearranged data:",encdata
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
else:
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed # decode using new testMap8 to get the original CryptProtect Data
while len(items) > 0: 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 if len(DB)>6:
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:
# store values used in decryption # store values used in decryption
DB['IDString'] = GetIDString() DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName() DB['UserName'] = GetUserName()
@ -1317,11 +1241,9 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"') pp = resline.find('\"Serial Number\" = \"')
@ -1330,31 +1252,24 @@ elif isosx:
sernums.append(sernum.strip()) sernums.append(sernum.strip())
return sernums return sernums
def GetUserHomeAppSupKindleDirParitionName(): def GetDiskPartitionNames():
home = os.getenv('HOME') names = []
dpath = home + '/Library'
cmdline = '/sbin/mount' cmdline = '/sbin/mount'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
disk = ''
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.startswith('/dev'): if resline.startswith('/dev'):
(devpart, mpath) = resline.split(' on ') (devpart, mpath) = resline.split(' on ')
dpart = devpart[5:] dpart = devpart[5:]
pp = mpath.find('(') names.append(dpart)
if pp >= 0: return names
mpath = mpath[:pp-1]
if dpath.startswith(mpath):
disk = dpart
return disk
# uses a sub process to get the UUID of the specified disk partition using ioreg # uses a sub process to get the UUID of all disk partitions
def GetDiskPartitionUUIDs(diskpart): def GetDiskPartitionUUIDs():
uuids = [] uuids = []
uuidnum = os.getenv('MYUUIDNUMBER') uuidnum = os.getenv('MYUUIDNUMBER')
if uuidnum != None: if uuidnum != None:
@ -1363,46 +1278,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
uuidnum = None
foundIt = False
nest = 0
uuidnest = -1
partnest = -2
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.find('{') >= 0:
nest += 1
if resline.find('}') >= 0:
nest -= 1
pp = resline.find('\"UUID\" = \"') pp = resline.find('\"UUID\" = \"')
if pp >= 0: if pp >= 0:
uuidnum = resline[pp+10:-1] uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip() uuidnum = uuidnum.strip()
uuidnest = nest uuids.append(uuidnum)
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)
return uuids return uuids
def GetMACAddressesMunged(): def GetMACAddressesMunged():
@ -1410,28 +1295,26 @@ elif isosx:
macnum = os.getenv('MYMACNUM') macnum = os.getenv('MYMACNUM')
if macnum != None: if macnum != None:
macnums.append(macnum) macnums.append(macnum)
cmdline = '/sbin/ifconfig en0' cmdline = 'networksetup -listallhardwareports' # en0'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
macnum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('ether ') pp = resline.find('Ethernet Address: ')
if pp >= 0: if pp >= 0:
macnum = resline[pp+6:-1] #print resline
macnum = resline[pp+18:]
macnum = macnum.strip() 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(':') maclst = macnum.split(':')
n = len(maclst) n = len(maclst)
if n != 6: if n != 6:
fountIt = False continue
break #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): for i in range(6):
maclst[i] = int('0x' + maclst[i], 0) maclst[i] = int('0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
@ -1442,16 +1325,15 @@ elif isosx:
mlst[1] = maclst[1] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 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]) 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 #print 'munged mac', macnum
break macnums.append(macnum)
if foundIt:
macnums.append(macnum)
return macnums return macnums
# uses unix env to get username instead of using sysctlbyname # uses unix env to get username instead of using sysctlbyname
def GetUserName(): def GetUserName():
username = os.getenv('USER') username = os.getenv('USER')
#print "Username:",username
return username return username
def GetIDStrings(): def GetIDStrings():
@ -1459,58 +1341,13 @@ elif isosx:
strings = [] strings = []
strings.extend(GetMACAddressesMunged()) strings.extend(GetMACAddressesMunged())
strings.extend(GetVolumesSerialNumbers()) strings.extend(GetVolumesSerialNumbers())
diskpart = GetUserHomeAppSupKindleDirParitionName() strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs(diskpart)) strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999') strings.append('9999999999')
#print strings #print "ID Strings:\n",strings
return 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 # unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0 # used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
@ -1528,8 +1365,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine # implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.9.0 class CryptUnprotectData(object):
class CryptUnprotectDataV3(object):
def __init__(self, entropy, IDString): def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + '+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2) passwdData = encode(SHA256(sp),charMap2)
@ -1598,219 +1434,117 @@ elif isosx:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
filehdr = infoReader.read(1)
filedata = infoReader.read() filedata = infoReader.read()
data = filedata[:-1]
items = data.split('/')
IDStrings = GetIDStrings() IDStrings = GetIDStrings()
for IDString in IDStrings: for IDString in IDStrings:
DB = {}
#print "trying IDString:",IDString #print "trying IDString:",IDString
try: try:
hdr = filehdr DB = {}
data = filedata items = data.split('/')
if data.find('[') != -1 :
# older style kindle-info file # the headerblob is the encrypted information needed to build the entropy string
cud = CryptUnprotectData(IDString) headerblob = items.pop(0)
items = data.split('[') encryptedValue = decode(headerblob, charMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '':
keyhash, rawdata = item.split(':')
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
elif hdr == '/':
# else newer style .kinf file used by K4Mac >= 1.6.0
# the .kinf file uses '/' to separate it into records
# so remove the trailing '/' to make it easy to use split
data = data[:-1]
items = data.split('/')
cud = CryptUnprotectDataV2(IDString)
# loop through the item records until all are processed # now extract the pieces in the same way
while len(items) > 0: # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group keyname = 'unknown'
# is the MD5 hash of the key name encoded by charMap5 for name in names:
keyhash = item[0:32] if encodeHash(name,testMap8) == keyhash:
keyname = 'unknown' keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# the raw keyhash string is also used to create entropy for the actual # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
# 'entropy' not used for K4Mac only K4PC # to the end to prevent decoding using testMap8 from
# entropy = SHA1(keyhash) # working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents) - largest prime number less than or equal to int(len(content)/3)
# of the number of records that follow # (in other words split 'about' 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value encdata = ''.join(edlst)
edlst = [] contlen = len(encdata)
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = 'unknown' # now properly split and recombine
for name in names: # by moving noffset chars from the start of the
if encodeHash(name,charMap5) == keyhash: # string to the end of the string
keyname = name noffset = contlen - primes(int(contlen/3))[-1]
break pfx = encdata[0:noffset]
if keyname == 'unknown': encdata = encdata[noffset:]
keyname = keyhash encdata = encdata + pfx
# the charMap5 encoded contents data has had a length # decode using testMap8 to get the CryptProtect Data
# of chars (always odd) cut off of the front and moved encryptedValue = decode(encdata,testMap8)
# to the end to prevent decoding using charMap5 from cleartext = cud.decrypt(encryptedValue)
# working properly, and thereby preventing the ensuing # print keyname
# CryptUnprotectData call from succeeding. # print cleartext
if len(cleartext) > 0:
DB[keyname] = cleartext
# The offset into the charMap5 encoded contents seems to be: if len(DB)>6:
# len(contents) - largest prime number less than or equal to int(len(content)/3) break
# (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
except: except:
pass pass
if len(DB)>4: if len(DB)>6:
# store values used in decryption # store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString DB['IDString'] = IDString
@ -1874,7 +1608,7 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
@ -1904,7 +1638,7 @@ def cli_main():
# save to the same directory as the script # save to the same directory as the script
outpath = os.path.dirname(argv[0]) 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)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):

View file

@ -24,8 +24,9 @@
# 6.4.0 - Fix for Kindle for PC encryption change # 6.4.0 - Fix for Kindle for PC encryption change
# 6.4.1 - Fix for new tags in Topaz ebooks # 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.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 sys
import os, os.path import os, os.path

View file

@ -48,6 +48,9 @@ __docformat__ = 'restructuredtext en'
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 # 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.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks. # 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_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]) 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. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@ -87,8 +90,12 @@ class SafeUnbuffered:
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,unicode):
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)

View file

@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label) data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10) 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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept) self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject) self.button_box.rejected.connect(self.reject)
@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property @property
def key_value(self): def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key return unicode(self.key_display.text()).strip()
return fetch_bandn_key(self.user_name,self.cc_number)
@property @property
def user_name(self): def user_name(self):
@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self): def cc_number(self):
return unicode(self.cc_ledit.text()).strip() 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): 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(): 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: if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!" errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) 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) QDialog.accept(self)
class AddEReaderDialog(QDialog): class AddEReaderDialog(QDialog):

View file

@ -4,7 +4,7 @@
from __future__ import with_statement from __future__ import with_statement
# kindlekey.py # 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: # Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 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.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode 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.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' __license__ = 'GPL v3'
__version__ = '1.9' __version__ = '2.2'
import sys, os, re import sys, os, re
from struct import pack, unpack, unpack_from 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 # or the python interface to the 32 vs 64 bit registry is broken
path = "" path = ""
if 'LOCALAPPDATA' in os.environ.keys(): 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%") path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative. # this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA') # path = getEnvironmentVariable('LOCALAPPDATA')
@ -994,192 +997,113 @@ if iswindows:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
DB = {} DB = {}
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
hdr = infoReader.read(1)
data = infoReader.read() 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 : # starts with an encoded and encrypted header blob
# older style kindle-info file headerblob = items.pop(0)
items = data.split('{') encryptedValue = decode(headerblob, testMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '': #print "header cleartext:",cleartext
keyhash, rawdata = item.split(':') # now extract the pieces that form the added entropy
keyname = "unknown" pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for name in names: for m in re.finditer(pattern, cleartext):
if encodeHash(name,charMap2) == keyhash: added_entropy = m.group(2) + m.group(4)
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
elif hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group # key names now use the new testMap8 encoding
# is the MD5 hash of the key name encoded by charMap5 keyname = "unknown"
keyhash = item[0:32] 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 # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
entropy = SHA1(keyhash) # 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 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents)-largest prime number <= int(len(content)/3)
# of the number of records that follow # (in other words split "about" 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value # by moving noffset chars from the start of the
edlst = [] # string to the end of the string
for i in xrange(rcnt): encdata = "".join(edlst)
item = items.pop(0) #print "encrypted data:",encdata
edlst.append(item) contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
keyname = "unknown" pfx = encdata[0:noffset]
for name in names: encdata = encdata[noffset:]
if encodeHash(name,charMap5) == keyhash: encdata = encdata + pfx
keyname = name #print "rearranged data:",encdata
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
else:
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed # decode using new testMap8 to get the original CryptProtect Data
while len(items) > 0: 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 if len(DB)>6:
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:
# store values used in decryption # store values used in decryption
DB['IDString'] = GetIDString() DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName() DB['UserName'] = GetUserName()
@ -1317,11 +1241,9 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"') pp = resline.find('\"Serial Number\" = \"')
@ -1330,31 +1252,24 @@ elif isosx:
sernums.append(sernum.strip()) sernums.append(sernum.strip())
return sernums return sernums
def GetUserHomeAppSupKindleDirParitionName(): def GetDiskPartitionNames():
home = os.getenv('HOME') names = []
dpath = home + '/Library'
cmdline = '/sbin/mount' cmdline = '/sbin/mount'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
disk = ''
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.startswith('/dev'): if resline.startswith('/dev'):
(devpart, mpath) = resline.split(' on ') (devpart, mpath) = resline.split(' on ')
dpart = devpart[5:] dpart = devpart[5:]
pp = mpath.find('(') names.append(dpart)
if pp >= 0: return names
mpath = mpath[:pp-1]
if dpath.startswith(mpath):
disk = dpart
return disk
# uses a sub process to get the UUID of the specified disk partition using ioreg # uses a sub process to get the UUID of all disk partitions
def GetDiskPartitionUUIDs(diskpart): def GetDiskPartitionUUIDs():
uuids = [] uuids = []
uuidnum = os.getenv('MYUUIDNUMBER') uuidnum = os.getenv('MYUUIDNUMBER')
if uuidnum != None: if uuidnum != None:
@ -1363,46 +1278,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
uuidnum = None
foundIt = False
nest = 0
uuidnest = -1
partnest = -2
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.find('{') >= 0:
nest += 1
if resline.find('}') >= 0:
nest -= 1
pp = resline.find('\"UUID\" = \"') pp = resline.find('\"UUID\" = \"')
if pp >= 0: if pp >= 0:
uuidnum = resline[pp+10:-1] uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip() uuidnum = uuidnum.strip()
uuidnest = nest uuids.append(uuidnum)
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)
return uuids return uuids
def GetMACAddressesMunged(): def GetMACAddressesMunged():
@ -1410,28 +1295,26 @@ elif isosx:
macnum = os.getenv('MYMACNUM') macnum = os.getenv('MYMACNUM')
if macnum != None: if macnum != None:
macnums.append(macnum) macnums.append(macnum)
cmdline = '/sbin/ifconfig en0' cmdline = 'networksetup -listallhardwareports' # en0'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
macnum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('ether ') pp = resline.find('Ethernet Address: ')
if pp >= 0: if pp >= 0:
macnum = resline[pp+6:-1] #print resline
macnum = resline[pp+18:]
macnum = macnum.strip() 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(':') maclst = macnum.split(':')
n = len(maclst) n = len(maclst)
if n != 6: if n != 6:
fountIt = False continue
break #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): for i in range(6):
maclst[i] = int('0x' + maclst[i], 0) maclst[i] = int('0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
@ -1442,16 +1325,15 @@ elif isosx:
mlst[1] = maclst[1] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 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]) 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 #print 'munged mac', macnum
break macnums.append(macnum)
if foundIt:
macnums.append(macnum)
return macnums return macnums
# uses unix env to get username instead of using sysctlbyname # uses unix env to get username instead of using sysctlbyname
def GetUserName(): def GetUserName():
username = os.getenv('USER') username = os.getenv('USER')
#print "Username:",username
return username return username
def GetIDStrings(): def GetIDStrings():
@ -1459,58 +1341,13 @@ elif isosx:
strings = [] strings = []
strings.extend(GetMACAddressesMunged()) strings.extend(GetMACAddressesMunged())
strings.extend(GetVolumesSerialNumbers()) strings.extend(GetVolumesSerialNumbers())
diskpart = GetUserHomeAppSupKindleDirParitionName() strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs(diskpart)) strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999') strings.append('9999999999')
#print strings #print "ID Strings:\n",strings
return 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 # unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0 # used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
@ -1528,8 +1365,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine # implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.9.0 class CryptUnprotectData(object):
class CryptUnprotectDataV3(object):
def __init__(self, entropy, IDString): def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + '+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2) passwdData = encode(SHA256(sp),charMap2)
@ -1598,219 +1434,117 @@ elif isosx:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
filehdr = infoReader.read(1)
filedata = infoReader.read() filedata = infoReader.read()
data = filedata[:-1]
items = data.split('/')
IDStrings = GetIDStrings() IDStrings = GetIDStrings()
for IDString in IDStrings: for IDString in IDStrings:
DB = {}
#print "trying IDString:",IDString #print "trying IDString:",IDString
try: try:
hdr = filehdr DB = {}
data = filedata items = data.split('/')
if data.find('[') != -1 :
# older style kindle-info file # the headerblob is the encrypted information needed to build the entropy string
cud = CryptUnprotectData(IDString) headerblob = items.pop(0)
items = data.split('[') encryptedValue = decode(headerblob, charMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '':
keyhash, rawdata = item.split(':')
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
elif hdr == '/':
# else newer style .kinf file used by K4Mac >= 1.6.0
# the .kinf file uses '/' to separate it into records
# so remove the trailing '/' to make it easy to use split
data = data[:-1]
items = data.split('/')
cud = CryptUnprotectDataV2(IDString)
# loop through the item records until all are processed # now extract the pieces in the same way
while len(items) > 0: # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group keyname = 'unknown'
# is the MD5 hash of the key name encoded by charMap5 for name in names:
keyhash = item[0:32] if encodeHash(name,testMap8) == keyhash:
keyname = 'unknown' keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# the raw keyhash string is also used to create entropy for the actual # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
# 'entropy' not used for K4Mac only K4PC # to the end to prevent decoding using testMap8 from
# entropy = SHA1(keyhash) # working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents) - largest prime number less than or equal to int(len(content)/3)
# of the number of records that follow # (in other words split 'about' 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value encdata = ''.join(edlst)
edlst = [] contlen = len(encdata)
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = 'unknown' # now properly split and recombine
for name in names: # by moving noffset chars from the start of the
if encodeHash(name,charMap5) == keyhash: # string to the end of the string
keyname = name noffset = contlen - primes(int(contlen/3))[-1]
break pfx = encdata[0:noffset]
if keyname == 'unknown': encdata = encdata[noffset:]
keyname = keyhash encdata = encdata + pfx
# the charMap5 encoded contents data has had a length # decode using testMap8 to get the CryptProtect Data
# of chars (always odd) cut off of the front and moved encryptedValue = decode(encdata,testMap8)
# to the end to prevent decoding using charMap5 from cleartext = cud.decrypt(encryptedValue)
# working properly, and thereby preventing the ensuing # print keyname
# CryptUnprotectData call from succeeding. # print cleartext
if len(cleartext) > 0:
DB[keyname] = cleartext
# The offset into the charMap5 encoded contents seems to be: if len(DB)>6:
# len(contents) - largest prime number less than or equal to int(len(content)/3) break
# (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
except: except:
pass pass
if len(DB)>4: if len(DB)>6:
# store values used in decryption # store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString DB['IDString'] = IDString
@ -1874,7 +1608,7 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
@ -1904,7 +1638,7 @@ def cli_main():
# save to the same directory as the script # save to the same directory as the script
outpath = os.path.dirname(argv[0]) 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)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):

View file

@ -49,6 +49,8 @@ __docformat__ = 'restructuredtext en'
# 6.4.0 - Updated for new Kindle for PC encryption # 6.4.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks. # 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.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_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]) 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. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@ -88,8 +90,12 @@ class SafeUnbuffered:
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,unicode):
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)

View file

@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label) data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10) 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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept) self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject) self.button_box.rejected.connect(self.reject)
@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property @property
def key_value(self): def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key return unicode(self.key_display.text()).strip()
return fetch_bandn_key(self.user_name,self.cc_number)
@property @property
def user_name(self): def user_name(self):
@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self): def cc_number(self):
return unicode(self.cc_ledit.text()).strip() 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): 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(): 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: if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!" errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) 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) QDialog.accept(self)
class AddEReaderDialog(QDialog): class AddEReaderDialog(QDialog):

View file

@ -4,7 +4,7 @@
from __future__ import with_statement from __future__ import with_statement
# kindlekey.py # 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: # Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 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.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode 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.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' __license__ = 'GPL v3'
__version__ = '1.9' __version__ = '2.2'
import sys, os, re import sys, os, re
from struct import pack, unpack, unpack_from 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 # or the python interface to the 32 vs 64 bit registry is broken
path = "" path = ""
if 'LOCALAPPDATA' in os.environ.keys(): 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%") path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative. # this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA') # path = getEnvironmentVariable('LOCALAPPDATA')
@ -994,192 +997,113 @@ if iswindows:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
DB = {} DB = {}
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
hdr = infoReader.read(1)
data = infoReader.read() 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 : # starts with an encoded and encrypted header blob
# older style kindle-info file headerblob = items.pop(0)
items = data.split('{') encryptedValue = decode(headerblob, testMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '': #print "header cleartext:",cleartext
keyhash, rawdata = item.split(':') # now extract the pieces that form the added entropy
keyname = "unknown" pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for name in names: for m in re.finditer(pattern, cleartext):
if encodeHash(name,charMap2) == keyhash: added_entropy = m.group(2) + m.group(4)
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
elif hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group # key names now use the new testMap8 encoding
# is the MD5 hash of the key name encoded by charMap5 keyname = "unknown"
keyhash = item[0:32] 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 # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
entropy = SHA1(keyhash) # 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 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents)-largest prime number <= int(len(content)/3)
# of the number of records that follow # (in other words split "about" 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value # by moving noffset chars from the start of the
edlst = [] # string to the end of the string
for i in xrange(rcnt): encdata = "".join(edlst)
item = items.pop(0) #print "encrypted data:",encdata
edlst.append(item) contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
keyname = "unknown" pfx = encdata[0:noffset]
for name in names: encdata = encdata[noffset:]
if encodeHash(name,charMap5) == keyhash: encdata = encdata + pfx
keyname = name #print "rearranged data:",encdata
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
else:
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed # decode using new testMap8 to get the original CryptProtect Data
while len(items) > 0: 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 if len(DB)>6:
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:
# store values used in decryption # store values used in decryption
DB['IDString'] = GetIDString() DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName() DB['UserName'] = GetUserName()
@ -1317,11 +1241,9 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"') pp = resline.find('\"Serial Number\" = \"')
@ -1330,31 +1252,24 @@ elif isosx:
sernums.append(sernum.strip()) sernums.append(sernum.strip())
return sernums return sernums
def GetUserHomeAppSupKindleDirParitionName(): def GetDiskPartitionNames():
home = os.getenv('HOME') names = []
dpath = home + '/Library'
cmdline = '/sbin/mount' cmdline = '/sbin/mount'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
disk = ''
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.startswith('/dev'): if resline.startswith('/dev'):
(devpart, mpath) = resline.split(' on ') (devpart, mpath) = resline.split(' on ')
dpart = devpart[5:] dpart = devpart[5:]
pp = mpath.find('(') names.append(dpart)
if pp >= 0: return names
mpath = mpath[:pp-1]
if dpath.startswith(mpath):
disk = dpart
return disk
# uses a sub process to get the UUID of the specified disk partition using ioreg # uses a sub process to get the UUID of all disk partitions
def GetDiskPartitionUUIDs(diskpart): def GetDiskPartitionUUIDs():
uuids = [] uuids = []
uuidnum = os.getenv('MYUUIDNUMBER') uuidnum = os.getenv('MYUUIDNUMBER')
if uuidnum != None: if uuidnum != None:
@ -1363,46 +1278,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
uuidnum = None
foundIt = False
nest = 0
uuidnest = -1
partnest = -2
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.find('{') >= 0:
nest += 1
if resline.find('}') >= 0:
nest -= 1
pp = resline.find('\"UUID\" = \"') pp = resline.find('\"UUID\" = \"')
if pp >= 0: if pp >= 0:
uuidnum = resline[pp+10:-1] uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip() uuidnum = uuidnum.strip()
uuidnest = nest uuids.append(uuidnum)
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)
return uuids return uuids
def GetMACAddressesMunged(): def GetMACAddressesMunged():
@ -1410,28 +1295,26 @@ elif isosx:
macnum = os.getenv('MYMACNUM') macnum = os.getenv('MYMACNUM')
if macnum != None: if macnum != None:
macnums.append(macnum) macnums.append(macnum)
cmdline = '/sbin/ifconfig en0' cmdline = 'networksetup -listallhardwareports' # en0'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
macnum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('ether ') pp = resline.find('Ethernet Address: ')
if pp >= 0: if pp >= 0:
macnum = resline[pp+6:-1] #print resline
macnum = resline[pp+18:]
macnum = macnum.strip() 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(':') maclst = macnum.split(':')
n = len(maclst) n = len(maclst)
if n != 6: if n != 6:
fountIt = False continue
break #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): for i in range(6):
maclst[i] = int('0x' + maclst[i], 0) maclst[i] = int('0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
@ -1442,16 +1325,15 @@ elif isosx:
mlst[1] = maclst[1] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 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]) 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 #print 'munged mac', macnum
break macnums.append(macnum)
if foundIt:
macnums.append(macnum)
return macnums return macnums
# uses unix env to get username instead of using sysctlbyname # uses unix env to get username instead of using sysctlbyname
def GetUserName(): def GetUserName():
username = os.getenv('USER') username = os.getenv('USER')
#print "Username:",username
return username return username
def GetIDStrings(): def GetIDStrings():
@ -1459,58 +1341,13 @@ elif isosx:
strings = [] strings = []
strings.extend(GetMACAddressesMunged()) strings.extend(GetMACAddressesMunged())
strings.extend(GetVolumesSerialNumbers()) strings.extend(GetVolumesSerialNumbers())
diskpart = GetUserHomeAppSupKindleDirParitionName() strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs(diskpart)) strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999') strings.append('9999999999')
#print strings #print "ID Strings:\n",strings
return 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 # unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0 # used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
@ -1528,8 +1365,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine # implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.9.0 class CryptUnprotectData(object):
class CryptUnprotectDataV3(object):
def __init__(self, entropy, IDString): def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + '+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2) passwdData = encode(SHA256(sp),charMap2)
@ -1598,219 +1434,117 @@ elif isosx:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
filehdr = infoReader.read(1)
filedata = infoReader.read() filedata = infoReader.read()
data = filedata[:-1]
items = data.split('/')
IDStrings = GetIDStrings() IDStrings = GetIDStrings()
for IDString in IDStrings: for IDString in IDStrings:
DB = {}
#print "trying IDString:",IDString #print "trying IDString:",IDString
try: try:
hdr = filehdr DB = {}
data = filedata items = data.split('/')
if data.find('[') != -1 :
# older style kindle-info file # the headerblob is the encrypted information needed to build the entropy string
cud = CryptUnprotectData(IDString) headerblob = items.pop(0)
items = data.split('[') encryptedValue = decode(headerblob, charMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '':
keyhash, rawdata = item.split(':')
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
elif hdr == '/':
# else newer style .kinf file used by K4Mac >= 1.6.0
# the .kinf file uses '/' to separate it into records
# so remove the trailing '/' to make it easy to use split
data = data[:-1]
items = data.split('/')
cud = CryptUnprotectDataV2(IDString)
# loop through the item records until all are processed # now extract the pieces in the same way
while len(items) > 0: # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group keyname = 'unknown'
# is the MD5 hash of the key name encoded by charMap5 for name in names:
keyhash = item[0:32] if encodeHash(name,testMap8) == keyhash:
keyname = 'unknown' keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# the raw keyhash string is also used to create entropy for the actual # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
# 'entropy' not used for K4Mac only K4PC # to the end to prevent decoding using testMap8 from
# entropy = SHA1(keyhash) # working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents) - largest prime number less than or equal to int(len(content)/3)
# of the number of records that follow # (in other words split 'about' 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value encdata = ''.join(edlst)
edlst = [] contlen = len(encdata)
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = 'unknown' # now properly split and recombine
for name in names: # by moving noffset chars from the start of the
if encodeHash(name,charMap5) == keyhash: # string to the end of the string
keyname = name noffset = contlen - primes(int(contlen/3))[-1]
break pfx = encdata[0:noffset]
if keyname == 'unknown': encdata = encdata[noffset:]
keyname = keyhash encdata = encdata + pfx
# the charMap5 encoded contents data has had a length # decode using testMap8 to get the CryptProtect Data
# of chars (always odd) cut off of the front and moved encryptedValue = decode(encdata,testMap8)
# to the end to prevent decoding using charMap5 from cleartext = cud.decrypt(encryptedValue)
# working properly, and thereby preventing the ensuing # print keyname
# CryptUnprotectData call from succeeding. # print cleartext
if len(cleartext) > 0:
DB[keyname] = cleartext
# The offset into the charMap5 encoded contents seems to be: if len(DB)>6:
# len(contents) - largest prime number less than or equal to int(len(content)/3) break
# (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
except: except:
pass pass
if len(DB)>4: if len(DB)>6:
# store values used in decryption # store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString DB['IDString'] = IDString
@ -1874,7 +1608,7 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
@ -1904,7 +1638,7 @@ def cli_main():
# save to the same directory as the script # save to the same directory as the script
outpath = os.path.dirname(argv[0]) 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)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):

View file

@ -4,7 +4,7 @@
from __future__ import with_statement from __future__ import with_statement
# kindlekey.py # 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: # Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 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.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode 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.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' __license__ = 'GPL v3'
__version__ = '1.9' __version__ = '2.2'
import sys, os, re import sys, os, re
from struct import pack, unpack, unpack_from 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 # or the python interface to the 32 vs 64 bit registry is broken
path = "" path = ""
if 'LOCALAPPDATA' in os.environ.keys(): 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%") path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative. # this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA') # path = getEnvironmentVariable('LOCALAPPDATA')
@ -994,192 +997,113 @@ if iswindows:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
DB = {} DB = {}
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
hdr = infoReader.read(1)
data = infoReader.read() 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 : # starts with an encoded and encrypted header blob
# older style kindle-info file headerblob = items.pop(0)
items = data.split('{') encryptedValue = decode(headerblob, testMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '': #print "header cleartext:",cleartext
keyhash, rawdata = item.split(':') # now extract the pieces that form the added entropy
keyname = "unknown" pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for name in names: for m in re.finditer(pattern, cleartext):
if encodeHash(name,charMap2) == keyhash: added_entropy = m.group(2) + m.group(4)
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
elif hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group # key names now use the new testMap8 encoding
# is the MD5 hash of the key name encoded by charMap5 keyname = "unknown"
keyhash = item[0:32] 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 # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
entropy = SHA1(keyhash) # 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 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents)-largest prime number <= int(len(content)/3)
# of the number of records that follow # (in other words split "about" 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value # by moving noffset chars from the start of the
edlst = [] # string to the end of the string
for i in xrange(rcnt): encdata = "".join(edlst)
item = items.pop(0) #print "encrypted data:",encdata
edlst.append(item) contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
keyname = "unknown" pfx = encdata[0:noffset]
for name in names: encdata = encdata[noffset:]
if encodeHash(name,charMap5) == keyhash: encdata = encdata + pfx
keyname = name #print "rearranged data:",encdata
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
else:
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed # decode using new testMap8 to get the original CryptProtect Data
while len(items) > 0: 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 if len(DB)>6:
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:
# store values used in decryption # store values used in decryption
DB['IDString'] = GetIDString() DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName() DB['UserName'] = GetUserName()
@ -1317,11 +1241,9 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"') pp = resline.find('\"Serial Number\" = \"')
@ -1330,31 +1252,24 @@ elif isosx:
sernums.append(sernum.strip()) sernums.append(sernum.strip())
return sernums return sernums
def GetUserHomeAppSupKindleDirParitionName(): def GetDiskPartitionNames():
home = os.getenv('HOME') names = []
dpath = home + '/Library'
cmdline = '/sbin/mount' cmdline = '/sbin/mount'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
disk = ''
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.startswith('/dev'): if resline.startswith('/dev'):
(devpart, mpath) = resline.split(' on ') (devpart, mpath) = resline.split(' on ')
dpart = devpart[5:] dpart = devpart[5:]
pp = mpath.find('(') names.append(dpart)
if pp >= 0: return names
mpath = mpath[:pp-1]
if dpath.startswith(mpath):
disk = dpart
return disk
# uses a sub process to get the UUID of the specified disk partition using ioreg # uses a sub process to get the UUID of all disk partitions
def GetDiskPartitionUUIDs(diskpart): def GetDiskPartitionUUIDs():
uuids = [] uuids = []
uuidnum = os.getenv('MYUUIDNUMBER') uuidnum = os.getenv('MYUUIDNUMBER')
if uuidnum != None: if uuidnum != None:
@ -1363,46 +1278,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
bsdname = None
uuidnum = None
foundIt = False
nest = 0
uuidnest = -1
partnest = -2
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
if resline.find('{') >= 0:
nest += 1
if resline.find('}') >= 0:
nest -= 1
pp = resline.find('\"UUID\" = \"') pp = resline.find('\"UUID\" = \"')
if pp >= 0: if pp >= 0:
uuidnum = resline[pp+10:-1] uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip() uuidnum = uuidnum.strip()
uuidnest = nest uuids.append(uuidnum)
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)
return uuids return uuids
def GetMACAddressesMunged(): def GetMACAddressesMunged():
@ -1410,28 +1295,26 @@ elif isosx:
macnum = os.getenv('MYMACNUM') macnum = os.getenv('MYMACNUM')
if macnum != None: if macnum != None:
macnums.append(macnum) macnums.append(macnum)
cmdline = '/sbin/ifconfig en0' cmdline = 'networksetup -listallhardwareports' # en0'
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split('\n')
cnt = len(reslst) cnt = len(reslst)
macnum = None
foundIt = False
for j in xrange(cnt): for j in xrange(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('ether ') pp = resline.find('Ethernet Address: ')
if pp >= 0: if pp >= 0:
macnum = resline[pp+6:-1] #print resline
macnum = resline[pp+18:]
macnum = macnum.strip() 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(':') maclst = macnum.split(':')
n = len(maclst) n = len(maclst)
if n != 6: if n != 6:
fountIt = False continue
break #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): for i in range(6):
maclst[i] = int('0x' + maclst[i], 0) maclst[i] = int('0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
@ -1442,16 +1325,15 @@ elif isosx:
mlst[1] = maclst[1] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 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]) 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 #print 'munged mac', macnum
break macnums.append(macnum)
if foundIt:
macnums.append(macnum)
return macnums return macnums
# uses unix env to get username instead of using sysctlbyname # uses unix env to get username instead of using sysctlbyname
def GetUserName(): def GetUserName():
username = os.getenv('USER') username = os.getenv('USER')
#print "Username:",username
return username return username
def GetIDStrings(): def GetIDStrings():
@ -1459,58 +1341,13 @@ elif isosx:
strings = [] strings = []
strings.extend(GetMACAddressesMunged()) strings.extend(GetMACAddressesMunged())
strings.extend(GetVolumesSerialNumbers()) strings.extend(GetVolumesSerialNumbers())
diskpart = GetUserHomeAppSupKindleDirParitionName() strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs(diskpart)) strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999') strings.append('9999999999')
#print strings #print "ID Strings:\n",strings
return 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 # unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0 # used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
@ -1528,8 +1365,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine # implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.9.0 class CryptUnprotectData(object):
class CryptUnprotectDataV3(object):
def __init__(self, entropy, IDString): def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + '+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2) passwdData = encode(SHA256(sp),charMap2)
@ -1598,219 +1434,117 @@ elif isosx:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ 'kindle.account.tokens',\
'kindle.cookie.item',\ 'kindle.cookie.item',\
'eulaVersionAccepted',\ 'eulaVersionAccepted',\
'login_date',\ 'login_date',\
'kindle.token.item',\ 'kindle.token.item',\
'login',\ 'login',\
'kindle.key.item',\ 'kindle.key.item',\
'kindle.name.info',\ 'kindle.name.info',\
'kindle.device.info',\ 'kindle.device.info',\
'MazamaRandomNumber',\ 'MazamaRandomNumber',\
'max_date',\ 'max_date',\
'SIGVERIF',\ 'SIGVERIF',\
'build_version',\ 'build_version',\
] ]
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
filehdr = infoReader.read(1)
filedata = infoReader.read() filedata = infoReader.read()
data = filedata[:-1]
items = data.split('/')
IDStrings = GetIDStrings() IDStrings = GetIDStrings()
for IDString in IDStrings: for IDString in IDStrings:
DB = {}
#print "trying IDString:",IDString #print "trying IDString:",IDString
try: try:
hdr = filehdr DB = {}
data = filedata items = data.split('/')
if data.find('[') != -1 :
# older style kindle-info file # the headerblob is the encrypted information needed to build the entropy string
cud = CryptUnprotectData(IDString) headerblob = items.pop(0)
items = data.split('[') encryptedValue = decode(headerblob, charMap1)
for item in items: cleartext = UnprotectHeaderData(encryptedValue)
if item != '':
keyhash, rawdata = item.split(':')
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
elif hdr == '/':
# else newer style .kinf file used by K4Mac >= 1.6.0
# the .kinf file uses '/' to separate it into records
# so remove the trailing '/' to make it easy to use split
data = data[:-1]
items = data.split('/')
cud = CryptUnprotectDataV2(IDString)
# loop through the item records until all are processed # now extract the pieces in the same way
while len(items) > 0: # 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) item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group keyname = 'unknown'
# is the MD5 hash of the key name encoded by charMap5 for name in names:
keyhash = item[0:32] if encodeHash(name,testMap8) == keyhash:
keyname = 'unknown' keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# the raw keyhash string is also used to create entropy for the actual # the testMap8 encoded contents data has had a length
# CryptProtectData Blob that represents that keys contents # of chars (always odd) cut off of the front and moved
# 'entropy' not used for K4Mac only K4PC # to the end to prevent decoding using testMap8 from
# entropy = SHA1(keyhash) # working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5 # The offset into the testMap8 encoded contents seems to be:
# has the ':' split char followed by the string representation # len(contents) - largest prime number less than or equal to int(len(content)/3)
# of the number of records that follow # (in other words split 'about' 2/3rds of the way through)
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data # move first offsets chars to end to align for decode by testMap8
# that make up the contents value encdata = ''.join(edlst)
edlst = [] contlen = len(encdata)
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = 'unknown' # now properly split and recombine
for name in names: # by moving noffset chars from the start of the
if encodeHash(name,charMap5) == keyhash: # string to the end of the string
keyname = name noffset = contlen - primes(int(contlen/3))[-1]
break pfx = encdata[0:noffset]
if keyname == 'unknown': encdata = encdata[noffset:]
keyname = keyhash encdata = encdata + pfx
# the charMap5 encoded contents data has had a length # decode using testMap8 to get the CryptProtect Data
# of chars (always odd) cut off of the front and moved encryptedValue = decode(encdata,testMap8)
# to the end to prevent decoding using charMap5 from cleartext = cud.decrypt(encryptedValue)
# working properly, and thereby preventing the ensuing # print keyname
# CryptUnprotectData call from succeeding. # print cleartext
if len(cleartext) > 0:
DB[keyname] = cleartext
# The offset into the charMap5 encoded contents seems to be: if len(DB)>6:
# len(contents) - largest prime number less than or equal to int(len(content)/3) break
# (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
except: except:
pass pass
if len(DB)>4: if len(DB)>6:
# store values used in decryption # store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString DB['IDString'] = IDString
@ -1874,7 +1608,7 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
@ -1904,7 +1638,7 @@ def cli_main():
# save to the same directory as the script # save to the same directory as the script
outpath = os.path.dirname(argv[0]) 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)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):