mirror of
https://github.com/apprenticeharper/DeDRM_tools
synced 2024-12-27 09:58:59 +01:00
Fixes for B&N key generation and Macs with bonded ethernet ports
This commit is contained in:
parent
eaa7a1afed
commit
3a931dfc90
12 changed files with 973 additions and 1944 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Binary file not shown.
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue