Compare commits

..

5 Commits

Author SHA1 Message Date
Apprentice Harper
3a931dfc90 Fixes for B&N key generation and Macs with bonded ethernet ports 2016-04-25 17:49:06 +01:00
Apprentice Harper
eaa7a1afed Switch to notifications for Mac App. Fix problem with Android backup files being missing, 2016-04-25 06:39:20 +01:00
Apprentice Harper
dc5261870f Topaz fixes to Mac & Windows apps, and version number update 2016-04-18 17:39:17 +01:00
Apprentice Harper
a2ba5005c9 Another Topaz missing token fix. 2016-04-18 16:54:46 +01:00
Apprentice Harper
24922999dc Fix for Topaz books of no more than two text pages. 2016-04-14 17:35:48 +01:00
22 changed files with 1039 additions and 1962 deletions

View File

@@ -1,16 +1,29 @@
{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460
{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 STHeitiSC-Light;}
{\colortbl;\red255\green255\blue255;}
\paperw11900\paperh16840\vieww12000\viewh15840\viewkind0
\deftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qc
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qc\partightenfactor0
\f0\b\fs24 \cf0 DeDRM ReadMe
\b0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\cf0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qj
\cf0 DeDRM is an application that packs all of the python dm removal software into one easy to use program that remembers preferences and settings.\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b \cf0 First Use for Mac OS X 10.9 and later\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b0 \cf0 The application is not signed, so the first time you run it you will need to change your security options, or hold down the option key when double-clicking on the icon, or control-click or right-button to get the contextual menu to open it.\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b \cf0 \
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\qj\partightenfactor0
\b0 \cf0 \
\
DeDRM is an application that packs all of the python dm removal software into one easy to use program that remembers preferences and settings.\
It works without manual configuration with Kindle for Mac ebooks, Adobe Digital Editions Adept ePub and PDF ebooks, and Barnes & Noble NOOK Study ebooks.\
\
To remove the DRM of Kindle ebooks from eInk Kindles, Kindle for Android, other Barnes & Noble ePubs, eReader pdb ebooks, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences, depending on the origin of your ebook files:\
@@ -44,35 +57,18 @@ A final preference is the destination folder for the DRM-free copies of your ebo
\
Once these preferences have been set, you can drag and drop ebooks (or folders of ebooks) onto the DeDRM droplet to remove the DRM.\
\
This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
This program uses notifications, so really needs Mac OS X 10.8 or above. It will not work on Mac OS X 10.4 or earlier. It might work on Mac OS C 10.5-10.7, but the latest Kindle for Mac does not support those System versions.\
\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b \cf0 Installation
\b0 \
Mac OS X 10.4
\i only
\i0 : You
\i must
\i0 first install Python 2.7.3 or later 2.7.x version from {\field{\*\fldinst{HYPERLINK "http://python.org/"}}{\fldrslt http://python.org/}}. At the time of writing, the direct download link is {\field{\*\fldinst{HYPERLINK "http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg"}}{\fldrslt http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg}}.\
\
Mac OS X 10.5 and above: You do
\i not
\i0 need to install Python.\
\
Drag the DeDRM application from from the DeDRM_Application_Macintosh folder (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
\
\
\b First Use for Mac OS X 10.9 and later\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b0 \cf0 The application is not signed, so the first time you run it you will need to change your security options, or hold down the option key when double-clicking on the icon, or control-click or right-button to get the contextual menu to open it.\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b \cf0 \
\
\b \
Use
\b0 \
1. To set the preferences, double-click the application and follow the instructions in the dialogs.\
@@ -81,15 +77,16 @@ Use
\
\b Troubleshooting\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b0 \cf0 A log is created on your desktop (DeDRM.log) containing detailed information from all the scripts. If you have any problems decrypting your ebooks, copy the contents of this log in a comment at Apprentice Alf's blog.\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://apprenticealf.wordpress.com/"}}{\fldrslt \cf0 http://apprenticealf.wordpress.com/}}\
\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b Credits\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720
\b \cf0 Credits\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\partightenfactor0
\b0 \cf0 The original inept and ignoble scripts were by i
\f1 \uc0\u9829

View File

@@ -24,7 +24,7 @@
<key>CFBundleExecutable</key>
<string>droplet</string>
<key>CFBundleGetInfoString</key>
<string>DeDRM AppleScript 6.4.1 Written 20102016 by Apprentice Alf et al.</string>
<string>DeDRM AppleScript 6.4.3 Written 20102016 by Apprentice Alf et al.</string>
<key>CFBundleIconFile</key>
<string>DeDRM</string>
<key>CFBundleIdentifier</key>
@@ -36,7 +36,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>6.4.1</string>
<string>6.4.3</string>
<key>CFBundleSignature</key>
<string>dplt</string>
<key>LSRequiresCarbon</key>

View File

@@ -48,6 +48,9 @@ __docformat__ = 'restructuredtext en'
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
# 6.4.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks.
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
# 6.4.3 - Fix for error that only appears when not in debug mode
# Also includes fix for Macs with bonded ethernet ports
"""
@@ -55,7 +58,7 @@ Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 4, 1)
PLUGIN_VERSION_TUPLE = (6, 4, 3)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -87,8 +90,12 @@ class SafeUnbuffered:
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr):
return getattr(self.stream, attr)

View File

@@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Retrieved key:", self))
self.key_display = QLabel(u"", self)
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
key_group.addWidget(self.key_display)
self.retrieve_button = QtGui.QPushButton(self)
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
self.retrieve_button.setText(u"Retrieve Key")
self.retrieve_button.clicked.connect(self.retrieve_key)
key_group.addWidget(self.retrieve_button)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
return unicode(self.key_display.text()).strip()
@property
def user_name(self):
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return unicode(self.cc_ledit.text()).strip()
def retrieve_key(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
if fetched_key == "":
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
else:
self.key_display.setText(fetched_key)
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_value) == 0:
self.retrieve_key()
if len(self.key_value) == 0:
return
QDialog.accept(self)
class AddEReaderDialog(QDialog):

View File

@@ -317,11 +317,16 @@ class PageParser(object):
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0),

View File

@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
# print "first normal text page is", spage
# get page height and width from first text page for use in stylesheet scaling
pname = 'page%04d.dat' % (pnum + 1)
pname = 'page%04d.dat' % (pnum - 1)
fname = os.path.join(pageDir,pname)
flat_xml = convert2xml.fromData(dict, fname)

View File

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

View File

@@ -23,8 +23,10 @@
# 6.3.6 - Version bump to match plugin
# 6.4.0 - Fix for Kindle for PC encryption change
# 6.4.1 - Fix for new tags in Topaz ebooks
# 6.4.2 - Fix for new tags in Topaz ebooks, and very small Topaz ebooks
# 6.4.3 - Version bump to match plugin & Mac app
__version__ = '6.4.1'
__version__ = '6.4.3'
import sys
import os, os.path

View File

@@ -48,6 +48,9 @@ __docformat__ = 'restructuredtext en'
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
# 6.4.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks.
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
# 6.4.3 - Fix for error that only appears when not in debug mode
# Also includes fix for Macs with bonded ethernet ports
"""
@@ -55,7 +58,7 @@ Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 4, 1)
PLUGIN_VERSION_TUPLE = (6, 4, 3)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -87,8 +90,12 @@ class SafeUnbuffered:
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr):
return getattr(self.stream, attr)

View File

@@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Retrieved key:", self))
self.key_display = QLabel(u"", self)
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
key_group.addWidget(self.key_display)
self.retrieve_button = QtGui.QPushButton(self)
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
self.retrieve_button.setText(u"Retrieve Key")
self.retrieve_button.clicked.connect(self.retrieve_key)
key_group.addWidget(self.retrieve_button)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
return unicode(self.key_display.text()).strip()
@property
def user_name(self):
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return unicode(self.cc_ledit.text()).strip()
def retrieve_key(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
if fetched_key == "":
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
else:
self.key_display.setText(fetched_key)
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_value) == 0:
self.retrieve_key()
if len(self.key_value) == 0:
return
QDialog.accept(self)
class AddEReaderDialog(QDialog):

View File

@@ -317,11 +317,16 @@ class PageParser(object):
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0),

View File

@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
# print "first normal text page is", spage
# get page height and width from first text page for use in stylesheet scaling
pname = 'page%04d.dat' % (pnum + 1)
pname = 'page%04d.dat' % (pnum - 1)
fname = os.path.join(pageDir,pname)
flat_xml = convert2xml.fromData(dict, fname)

View File

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

View File

@@ -48,6 +48,9 @@ __docformat__ = 'restructuredtext en'
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
# 6.4.0 - Updated for new Kindle for PC encryption
# 6.4.1 - Fix for some new tags in Topaz ebooks.
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
# 6.4.3 - Fix for error that only appears when not in debug mode
# Also includes fix for Macs with bonded ethernet ports
"""
@@ -55,7 +58,7 @@ Decrypt DRMed ebooks.
"""
PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 4, 1)
PLUGIN_VERSION_TUPLE = (6, 4, 3)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -87,8 +90,12 @@ class SafeUnbuffered:
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
self.stream.write(data)
self.stream.flush()
except:
# We can do nothing if a write fails
pass
def __getattr__(self, attr):
return getattr(self.stream, attr)

View File

@@ -566,6 +566,19 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"Retrieved key:", self))
self.key_display = QLabel(u"", self)
self.key_display.setToolTip(_(u"Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
key_group.addWidget(self.key_display)
self.retrieve_button = QtGui.QPushButton(self)
self.retrieve_button.setToolTip(_(u"Click to retrieve your B&N encryption key from the B&N servers"))
self.retrieve_button.setText(u"Retrieve Key")
self.retrieve_button.clicked.connect(self.retrieve_key)
key_group.addWidget(self.retrieve_button)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
@@ -579,8 +592,7 @@ class AddBandNKeyDialog(QDialog):
@property
def key_value(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
return fetch_bandn_key(self.user_name,self.cc_number)
return unicode(self.key_display.text()).strip()
@property
def user_name(self):
@@ -590,6 +602,14 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return unicode(self.cc_ledit.text()).strip()
def retrieve_key(self):
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
if fetched_key == "":
errmsg = u"Could not retrieve key. Check username, password and intenet connectivity and try again."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
else:
self.key_display.setText(fetched_key)
def accept(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
@@ -598,6 +618,10 @@ class AddBandNKeyDialog(QDialog):
if len(self.key_name) < 4:
errmsg = u"Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_value) == 0:
self.retrieve_key()
if len(self.key_value) == 0:
return
QDialog.accept(self)
class AddEReaderDialog(QDialog):

View File

@@ -317,11 +317,16 @@ class PageParser(object):
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0),

View File

@@ -385,7 +385,7 @@ def generateBook(bookDir, raw, fixedimage):
# print "first normal text page is", spage
# get page height and width from first text page for use in stylesheet scaling
pname = 'page%04d.dat' % (pnum + 1)
pname = 'page%04d.dat' % (pnum - 1)
fname = os.path.join(pageDir,pname)
flat_xml = convert2xml.fromData(dict, fname)

View File

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

View File

@@ -4,7 +4,7 @@
from __future__ import with_statement
# kindlekey.py
# Copyright © 2010-2015 by some_updates, Apprentice Alf and Apprentice Harper
# Copyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper
# Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
@@ -19,6 +19,9 @@ from __future__ import with_statement
# 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names
# 1.9 - Fixes for Unicode in Windows user names
# 2.0 - Added comments and extra fix for non-ascii Windows user names
# 2.1 - Fixed Kindle for PC encryption changes March 2016
# 2.2 - Fixes for Macs with bonded ethernet ports
# Also removed old .kinfo file support (pre-2011)
"""
@@ -26,7 +29,7 @@ Retrieve Kindle for PC/Mac user key.
"""
__license__ = 'GPL v3'
__version__ = '1.9'
__version__ = '2.2'
import sys, os, re
from struct import pack, unpack, unpack_from
@@ -926,7 +929,7 @@ if iswindows:
# or the python interface to the 32 vs 64 bit registry is broken
path = ""
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
# this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA')
@@ -993,179 +996,120 @@ if iswindows:
# determine type of kindle info provided and return a
# database of keynames and values
def getDBfromFile(kInfoFile):
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
names = [\
'kindle.account.tokens',\
'kindle.cookie.item',\
'eulaVersionAccepted',\
'login_date',\
'kindle.token.item',\
'login',\
'kindle.key.item',\
'kindle.name.info',\
'kindle.device.info',\
'MazamaRandomNumber',\
'max_date',\
'SIGVERIF',\
'build_version',\
]
DB = {}
with open(kInfoFile, 'rb') as infoReader:
hdr = infoReader.read(1)
data = infoReader.read()
# assume newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
if data.find('{') != -1 :
# older style kindle-info file
items = data.split('{')
for item in items:
if item != '':
keyhash, rawdata = item.split(':')
keyname = "unknown"
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
elif hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# starts with an encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
#print "header cleartext:",cleartext
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
item = items.pop(0)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
# the sha1 of raw keyhash string is used to create entropy along
# with the added entropy provided above from the headerblob
entropy = SHA1(keyhash) + added_entropy
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
# key names now use the new testMap8 encoding
keyname = "unknown"
for name in names:
if encodeHash(name,testMap8) == keyhash:
keyname = name
#print "keyname found from hash:",keyname
break
if keyname == "unknown":
keyname = keyhash
#print "keyname not found, hash is:",keyname
# the raw keyhash string is used to create entropy for the actual
# CryptProtectData Blob that represents that keys contents
entropy = SHA1(keyhash)
# the testMap8 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using testMap8 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# The offset into the testMap8 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = "unknown"
for name in names:
if encodeHash(name,charMap5) == keyhash:
keyname = name
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
else:
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# move first offsets chars to end to align for decode by testMap8
# by moving noffset chars from the start of the
# string to the end of the string
encdata = "".join(edlst)
#print "encrypted data:",encdata
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
#print "rearranged data:",encdata
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
item = items.pop(0)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
# the sha1 of raw keyhash string is used to create entropy along
# with the added entropy provided above from the headerblob
entropy = SHA1(keyhash) + added_entropy
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
# key names now use the new testMap8 encoding
keyname = "unknown"
for name in names:
if encodeHash(name,testMap8) == keyhash:
keyname = name
break
# 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)
# decode using new testMap8 to get the original CryptProtect Data
encryptedValue = decode(encdata,testMap8)
#print "decoded data:",encryptedValue.encode('hex')
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
if len(cleartext)>0:
#print "cleartext data:",cleartext,":end data"
DB[keyname] = cleartext
#print keyname, cleartext
if 'kindle.account.tokens' in DB:
if len(DB)>6:
# store values used in decryption
DB['IDString'] = GetIDString()
DB['UserName'] = GetUserName()
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex'))
else:
print u"Couldn't decrypt file."
DB = {}
return DB
elif isosx:
@@ -1297,11 +1241,9 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n')
cnt = len(reslst)
bsdname = None
sernum = None
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"')
@@ -1310,31 +1252,24 @@ elif isosx:
sernums.append(sernum.strip())
return sernums
def GetUserHomeAppSupKindleDirParitionName():
home = os.getenv('HOME')
dpath = home + '/Library'
def GetDiskPartitionNames():
names = []
cmdline = '/sbin/mount'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
reslst = out1.split('\n')
cnt = len(reslst)
disk = ''
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
if resline.startswith('/dev'):
(devpart, mpath) = resline.split(' on ')
dpart = devpart[5:]
pp = mpath.find('(')
if pp >= 0:
mpath = mpath[:pp-1]
if dpath.startswith(mpath):
disk = dpart
return disk
names.append(dpart)
return names
# uses a sub process to get the UUID of the specified disk partition using ioreg
def GetDiskPartitionUUIDs(diskpart):
# uses a sub process to get the UUID of all disk partitions
def GetDiskPartitionUUIDs():
uuids = []
uuidnum = os.getenv('MYUUIDNUMBER')
if uuidnum != None:
@@ -1343,46 +1278,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
#print out1
reslst = out1.split('\n')
cnt = len(reslst)
bsdname = None
uuidnum = None
foundIt = False
nest = 0
uuidnest = -1
partnest = -2
for j in xrange(cnt):
resline = reslst[j]
if resline.find('{') >= 0:
nest += 1
if resline.find('}') >= 0:
nest -= 1
pp = resline.find('\"UUID\" = \"')
if pp >= 0:
uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip()
uuidnest = nest
if partnest == uuidnest and uuidnest > 0:
foundIt = True
break
bb = resline.find('\"BSD Name\" = \"')
if bb >= 0:
bsdname = resline[bb+14:-1]
bsdname = bsdname.strip()
if (bsdname == diskpart):
partnest = nest
else :
partnest = -2
if partnest == uuidnest and partnest > 0:
foundIt = True
break
if nest == 0:
partnest = -2
uuidnest = -1
uuidnum = None
bsdname = None
if foundIt:
uuids.append(uuidnum)
uuids.append(uuidnum)
return uuids
def GetMACAddressesMunged():
@@ -1390,28 +1295,26 @@ elif isosx:
macnum = os.getenv('MYMACNUM')
if macnum != None:
macnums.append(macnum)
cmdline = '/sbin/ifconfig en0'
cmdline = 'networksetup -listallhardwareports' # en0'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate()
reslst = out1.split('\n')
cnt = len(reslst)
macnum = None
foundIt = False
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('ether ')
pp = resline.find('Ethernet Address: ')
if pp >= 0:
macnum = resline[pp+6:-1]
#print resline
macnum = resline[pp+18:]
macnum = macnum.strip()
# print 'original mac', macnum
# now munge it up the way Kindle app does
# by xoring it with 0xa5 and swapping elements 3 and 4
maclst = macnum.split(':')
n = len(maclst)
if n != 6:
fountIt = False
break
continue
#print 'original mac', macnum
# now munge it up the way Kindle app does
# by xoring it with 0xa5 and swapping elements 3 and 4
for i in range(6):
maclst[i] = int('0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
@@ -1422,16 +1325,15 @@ elif isosx:
mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 0xa5
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
foundIt = True
break
if foundIt:
macnums.append(macnum)
#print 'munged mac', macnum
macnums.append(macnum)
return macnums
# uses unix env to get username instead of using sysctlbyname
def GetUserName():
username = os.getenv('USER')
#print "Username:",username
return username
def GetIDStrings():
@@ -1439,58 +1341,13 @@ elif isosx:
strings = []
strings.extend(GetMACAddressesMunged())
strings.extend(GetVolumesSerialNumbers())
diskpart = GetUserHomeAppSupKindleDirParitionName()
strings.extend(GetDiskPartitionUUIDs(diskpart))
strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999')
#print strings
#print "ID Strings:\n",strings
return strings
# implements an Pseudo Mac Version of Windows built-in Crypto routine
# used by Kindle for Mac versions < 1.6.0
class CryptUnprotectData(object):
def __init__(self, IDString):
sp = IDString + '!@#' + GetUserName()
passwdData = encode(SHA256(sp),charMap1)
salt = '16743'
self.crp = LibCrypto()
iter = 0x3e8
keylen = 0x80
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
self.key = key_iv[0:32]
self.iv = key_iv[32:48]
self.crp.set_decrypt_key(self.key, self.iv)
def decrypt(self, encryptedData):
cleartext = self.crp.decrypt(encryptedData)
cleartext = decode(cleartext,charMap1)
return cleartext
# implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.6.0
class CryptUnprotectDataV2(object):
def __init__(self, IDString):
sp = GetUserName() + ':&%:' + IDString
passwdData = encode(SHA256(sp),charMap5)
# salt generation as per the code
salt = 0x0512981d * 2 * 1 * 1
salt = str(salt) + GetUserName()
salt = encode(salt,charMap5)
self.crp = LibCrypto()
iter = 0x800
keylen = 0x400
key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
self.key = key_iv[0:32]
self.iv = key_iv[32:48]
self.crp.set_decrypt_key(self.key, self.iv)
def decrypt(self, encryptedData):
cleartext = self.crp.decrypt(encryptedData)
cleartext = decode(cleartext, charMap5)
return cleartext
# unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData):
@@ -1508,8 +1365,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine
# used for Kindle for Mac Versions >= 1.9.0
class CryptUnprotectDataV3(object):
class CryptUnprotectData(object):
def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2)
@@ -1577,206 +1433,118 @@ elif isosx:
# determine type of kindle info provided and return a
# database of keynames and values
def getDBfromFile(kInfoFile):
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
names = [\
'kindle.account.tokens',\
'kindle.cookie.item',\
'eulaVersionAccepted',\
'login_date',\
'kindle.token.item',\
'login',\
'kindle.key.item',\
'kindle.name.info',\
'kindle.device.info',\
'MazamaRandomNumber',\
'max_date',\
'SIGVERIF',\
'build_version',\
]
with open(kInfoFile, 'rb') as infoReader:
filehdr = infoReader.read(1)
filedata = infoReader.read()
data = filedata[:-1]
items = data.split('/')
IDStrings = GetIDStrings()
for IDString in IDStrings:
DB = {}
#print "trying IDString:",IDString
try:
hdr = filehdr
data = filedata
if data.find('[') != -1 :
# older style kindle-info file
cud = CryptUnprotectData(IDString)
items = data.split('[')
for item in items:
if item != '':
keyhash, rawdata = item.split(':')
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
elif hdr == '/':
# else newer style .kinf file used by K4Mac >= 1.6.0
# the .kinf file uses '/' to separate it into records
# so remove the trailing '/' to make it easy to use split
data = data[:-1]
items = data.split('/')
cud = CryptUnprotectDataV2(IDString)
DB = {}
items = data.split('/')
# the headerblob is the encrypted information needed to build the entropy string
headerblob = items.pop(0)
encryptedValue = decode(headerblob, charMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# loop through the item records until all are processed
while len(items) > 0:
# now extract the pieces in the same way
# this version is different from K4PC it scales the build number by multipying by 735
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
# get the first item record
cud = CryptUnprotectData(entropy,IDString)
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
item = items.pop(0)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
keyname = 'unknown'
# unlike K4PC the keyhash is not used in generating entropy
# entropy = SHA1(keyhash) + added_entropy
# entropy = added_entropy
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
keyname = 'unknown'
keyname = 'unknown'
for name in names:
if encodeHash(name,testMap8) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# the raw keyhash string is also used to create entropy for the actual
# CryptProtectData Blob that represents that keys contents
# 'entropy' not used for K4Mac only K4PC
# entropy = SHA1(keyhash)
# the testMap8 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using testMap8 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# The offset into the testMap8 encoded contents seems to be:
# len(contents) - largest prime number less than or equal to int(len(content)/3)
# (in other words split 'about' 2/3rds of the way through)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
# move first offsets chars to end to align for decode by testMap8
encdata = ''.join(edlst)
contlen = len(encdata)
keyname = 'unknown'
for name in names:
if encodeHash(name,charMap5) == keyhash:
keyname = name
break
if keyname == 'unknown':
keyname = keyhash
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
noffset = contlen - primes(int(contlen/3))[-1]
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# decode using testMap8 to get the CryptProtect Data
encryptedValue = decode(encdata,testMap8)
cleartext = cud.decrypt(encryptedValue)
# print keyname
# print cleartext
if len(cleartext) > 0:
DB[keyname] = cleartext
# The offset into the charMap5 encoded contents seems to be:
# len(contents) - largest prime number less than or equal to int(len(content)/3)
# (in other words split 'about' 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = ''.join(edlst)
contlen = len(encdata)
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
noffset = contlen - primes(int(contlen/3))[-1]
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using charMap5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
cleartext = cud.decrypt(encryptedValue)
if len(cleartext) > 0:
DB[keyname] = cleartext
if 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
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 'MazamaRandomNumber' in DB and 'kindle.account.tokens' in DB:
break
if len(DB)>6:
break
except:
pass
if 'kindle.account.tokens' in DB:
if len(DB)>6:
# store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName())
DB['IDString'] = IDString
@@ -1840,7 +1608,7 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2013 some_updates and Apprentice Alf".format(progname,__version__)
print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
@@ -1870,7 +1638,7 @@ def cli_main():
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
# make sure the outpath is canonical
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):