mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 21:08:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f34a9a196 | ||
|
|
529dd3f160 | ||
|
|
4163d5ccf4 | ||
|
|
867ac35b45 | ||
|
|
427137b0fe | ||
|
|
ac9cdb1e98 | ||
|
|
2bedd75005 | ||
|
|
8b632e309f |
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.7
|
||||
# ineptpdf.pyw, version 7.9
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
@@ -33,6 +33,7 @@ from __future__ import with_statement
|
||||
# 7.6 - backported AES and other fixes from version 8.4.48
|
||||
# 7.7 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 7.8 - Modify interface to allow use of import
|
||||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
@@ -156,6 +157,7 @@ def _load_crypto_libcrypto():
|
||||
return out.raw
|
||||
|
||||
class AES(object):
|
||||
MODE_CBC = 0
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
self = AES()
|
||||
@@ -1531,16 +1533,30 @@ class PDFDocument(object):
|
||||
bookkey = bookkey[index:]
|
||||
ebx_V = int_value(param.get('V', 4))
|
||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||
# added because of the booktype / decryption book session key error
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
elif ebx_V < 4 or ebx_type < 6:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
# added because of improper booktype / decryption book session key errors
|
||||
if length > 0:
|
||||
if len(bookkey) == length:
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
elif len(bookkey) == length + 1:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
else:
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||
else:
|
||||
V = 2
|
||||
if length and len(bookkey) != length:
|
||||
raise ADEPTError('error decrypting book session key')
|
||||
# proper length unknown try with whatever you have
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
self.decrypt_key = bookkey
|
||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||
self.decipher = self.decrypt_rc4
|
||||
|
||||
116
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
116
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
# from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 3, 1) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM: Kindle info/kinf file %s found in config folder.' % filename
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM: Error reading kindle info/kinf files from config directory.'
|
||||
pass
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
mb = topazextract.TopazBook(path_to_ebook)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
mb.getMobiFile(of.name)
|
||||
else :
|
||||
of = self.temporary_file(bookname+'.htmlz')
|
||||
mb.getHTMLZip(of.name)
|
||||
mb.cleanup()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, use a comma (no spaces) to separate each PID or SerialNumber from the next.'
|
||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
||||
ys = []
|
||||
gdefs = []
|
||||
|
||||
# get path defintions, positions, dimensions for ecah glyph
|
||||
# get path defintions, positions, dimensions for each glyph
|
||||
# that makes up the image, and find min x and min y to reposition origin
|
||||
minx = -1
|
||||
miny = -1
|
||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
||||
lastGlyph = firstglyphList[last]
|
||||
else :
|
||||
lastGlyph = len(gidList)
|
||||
|
||||
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||
# by reverting to text based paragraph
|
||||
if firstGlyph >= lastGlyph:
|
||||
# revert to standard text based paragraph
|
||||
for wordnum in xrange(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||
glyphList.append(glyphnum)
|
||||
# include any extratokens if they exist
|
||||
|
||||
@@ -21,10 +21,21 @@ from struct import unpack
|
||||
|
||||
|
||||
# local support routines
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.k4mobidedrm import convert2xml
|
||||
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
|
||||
# Get a 7 bit encoded number from a file
|
||||
@@ -192,6 +203,8 @@ class GParser(object):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
def getGlyphDim(self, gly):
|
||||
if self.gdpi[gly] == 0:
|
||||
return 0, 0
|
||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||
return maxh, maxw
|
||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
print 'Processing Meta Data and creating OPF'
|
||||
meta_array = getMetaArray(metaFile)
|
||||
|
||||
# replace special chars in title and authors like & < >
|
||||
title = meta_array.get('Title','No Title Provided')
|
||||
title = title.replace('&','&')
|
||||
title = title.replace('<','<')
|
||||
title = title.replace('>','>')
|
||||
meta_array['Title'] = title
|
||||
authors = meta_array.get('Authors','No Authors Provided')
|
||||
authors = authors.replace('&','&')
|
||||
authors = authors.replace('<','<')
|
||||
authors = authors.replace('>','>')
|
||||
meta_array['Authors'] = authors
|
||||
|
||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||
metastr = ''
|
||||
for key in meta_array:
|
||||
@@ -399,8 +424,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||
htmlstr += '</head>\n<body>\n'
|
||||
|
||||
@@ -416,8 +443,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
svgindex += '</head>\n'
|
||||
svgindex += '<body>\n'
|
||||
|
||||
@@ -471,9 +500,12 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||
# adding metadata
|
||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
if 'GUID' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
if 'ASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
if 'oASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||
opfstr += ' <dc:language>en</dc:language>\n'
|
||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += ' </metadata>\n'
|
||||
opfstr += '<manifest>\n'
|
||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||
# adding image files to manifest
|
||||
filenames = os.listdir(imgDir)
|
||||
filenames = sorted(filenames)
|
||||
|
||||
199
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Normal file
199
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
|
||||
__version__ = '3.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import re
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert spaces to underscores
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace(' ','_')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = bookname
|
||||
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||
outfilename = outfilename + "_" + filenametitle
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
return 0
|
||||
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
mb.getXMLZip(zipname)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
global charMap3
|
||||
global charMap4
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
@@ -66,66 +78,7 @@ def decode(data,map):
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
def parseKindleInfo(kInfoFile):
|
||||
DB = {}
|
||||
infoReader = openKindleInfo(kInfoFile)
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
if sys.platform.startswith('win'):
|
||||
items = data.split('{')
|
||||
else :
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
if sys.platform.startswith('win'):
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
else:
|
||||
cleartext = CryptUnprotectData(encryptedValue)
|
||||
return decode(cleartext, charMap1)
|
||||
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForKey(key):
|
||||
global charMap2
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
# Find if the original string for a hashed/encoded string is known.
|
||||
# If so return the original string othwise return an empty string.
|
||||
def findNameForHash(hash):
|
||||
global charMap2
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return result
|
||||
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
@@ -224,13 +177,11 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
|
||||
if rec209 != None and token != None:
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
@@ -240,15 +191,15 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
return pidlst
|
||||
|
||||
|
||||
# Parse the EXTH header records and parse the Kindleinfo
|
||||
# file to calculate the book pid.
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
global kindleDatabase
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
print(message)
|
||||
kindleDatabase = None
|
||||
@@ -256,10 +207,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
||||
|
||||
@@ -275,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
|
||||
# Compute book PID
|
||||
if rec209 == None or token == None:
|
||||
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
|
||||
return pidlst
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
# Compute book PIDs
|
||||
|
||||
# book pid
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
@@ -305,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
pidlst = getK4Pids(pidlst, md1, md2)
|
||||
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||
for infoFile in kInfoFiles:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
for serialnum in serials:
|
||||
|
||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
@@ -18,10 +23,32 @@ from struct import unpack
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
import kgenpids
|
||||
import genbook
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
import kgenpids
|
||||
import genbook
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tdir, localfilePath)
|
||||
|
||||
#
|
||||
# Utility routines
|
||||
#
|
||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
||||
|
||||
|
||||
class TopazBook:
|
||||
def __init__(self, filename, outdir):
|
||||
def __init__(self, filename):
|
||||
self.fo = file(filename, 'rb')
|
||||
self.outdir = outdir
|
||||
self.outdir = tempfile.mkdtemp()
|
||||
self.bookPayloadOffset = 0
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
@@ -157,18 +184,22 @@ class TopazBook:
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
for i in range (0,nbRecords) :
|
||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
||||
self.bookMetadata[record[0]] = record[1]
|
||||
keyval = bookReadString(self.fo)
|
||||
content = bookReadString(self.fo)
|
||||
# print keyval
|
||||
# print content
|
||||
self.bookMetadata[keyval] = content
|
||||
return self.bookMetadata
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
keysRecord = None
|
||||
keysRecordRecord = None
|
||||
if 'keys' in self.bookMetadata:
|
||||
keysRecord = self.bookMetadata['keys']
|
||||
if keysRecord in self.bookMetadata:
|
||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
||||
keysRecord = self.bookMetadata.get('keys','')
|
||||
keysRecordRecord = ''
|
||||
if keysRecord != '':
|
||||
keylst = keysRecord.split(',')
|
||||
for keyval in keylst:
|
||||
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||
return keysRecord, keysRecordRecord
|
||||
|
||||
def getBookTitle(self):
|
||||
@@ -313,21 +344,33 @@ class TopazBook:
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.close()
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
@@ -379,58 +422,46 @@ def main(argv=sys.argv):
|
||||
return 1
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
tb = TopazBook(infile, tempdir)
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
tb.processBook(pidlst)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
tb.cleanup()
|
||||
return 1
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
tb.cleanup
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If yo
|
||||
|
||||
|
||||
This plugin is meant to remove the DRM from .prc, .azw, .azw1, and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Installation:
|
||||
Installation:
|
||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the 'Add' button. You're done.
|
||||
|
||||
Go to Calibre's Preferences page... click on the Plugins button. Click on the "Add a new plugin" button at the bottom of the screen. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button. Then click on the "Yes" button in the warning dialog that appears. A Confirmation dialog appears that says the plugin has been installed.
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
Installing openssl on Windows 64-bit (Windows 2000 and higher)
|
||||
|
||||
Win64 OpenSSL v0.9.8o (8Mb)
|
||||
http://www.slproweb.com/download/Win64OpenSSL-0_9_8o.exe
|
||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
||||
|
||||
Visual C++ 2008 Redistributables (x64) (1.7Mb)
|
||||
http://www.microsoft.com/downloads/details.aspx?familyid=bd2a6171-e2d6-4230-b809-9a8d7548c1b6
|
||||
|
||||
|
||||
|
||||
Installing openssl on Windows 32-bit (Windows 2000 and higher)
|
||||
|
||||
Win32 OpenSSL v0.9.8o (8Mb)
|
||||
http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe
|
||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
||||
|
||||
Visual C++ 2008 Redistributables (1.7Mb)
|
||||
http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF
|
||||
|
||||
|
||||
|
||||
Other versions of OpenSSL (and versions for Windows older than Windows 2000) can be found on the following website.
|
||||
|
||||
Shining Light Productions
|
||||
http://www.slproweb.com/products/Win32OpenSSL.html
|
||||
Binary file not shown.
@@ -31,10 +31,15 @@
|
||||
# 0.0.1 - Initial release
|
||||
# 0.0.2 - updated to distinguish it from earlier non-openssl version
|
||||
# 0.0.3 - removed added psyco code as it is not supported under Calibre's Python 2.7
|
||||
# 0.0.4 - minor typos fixed
|
||||
# 0.0.5 - updated to the new calibre plugin interface
|
||||
|
||||
import sys, os
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre_plugins.erdrpdb2pml import erdr2pml
|
||||
|
||||
class eRdrDeDRM(FileTypePlugin):
|
||||
name = 'eReader PDB 2 PML' # Name of the plugin
|
||||
@@ -42,16 +47,14 @@ class eRdrDeDRM(FileTypePlugin):
|
||||
Credit given to The Dark Reverser for the original standalone script.'
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer' # The author of this plugin
|
||||
version = (0, 0, 4) # The version number of this plugin
|
||||
version = (0, 0, 5) # The version number of this plugin
|
||||
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
minimum_calibre_version = (0, 7, 55)
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
global bookname, erdr2pml
|
||||
import erdr2pml
|
||||
|
||||
infile = path_to_ebook
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
@@ -57,8 +57,10 @@
|
||||
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||
# 0.17 - added support for pycrypto's DES as well
|
||||
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||
# 0.19 - Modify the interface to allow use of import
|
||||
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||
|
||||
__version__='0.18'
|
||||
__version__='0.20'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -70,32 +72,50 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
# first try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
if Des == None:
|
||||
# they try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
else:
|
||||
# first try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
if Des == None:
|
||||
# then try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
|
||||
# if that did not work then use pure python implementation
|
||||
# of DES and try to speed it up with Psycho
|
||||
if Des == None:
|
||||
import python_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import python_des
|
||||
else:
|
||||
import python_des
|
||||
Des = python_des.Des
|
||||
# Import Psyco if available
|
||||
try:
|
||||
@@ -111,12 +131,14 @@ except ImportError:
|
||||
# older Python release
|
||||
import sha
|
||||
sha1 = lambda s: sha.new(s)
|
||||
|
||||
import cgi
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
#logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class Sectionizer(object):
|
||||
def __init__(self, filename, ident):
|
||||
self.contents = file(filename, 'rb').read()
|
||||
@@ -364,7 +386,7 @@ def cleanPML(pml):
|
||||
def convertEreaderToPml(infile, name, cc, outdir):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
print " Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect.loadSection, name, cc)
|
||||
@@ -390,6 +412,47 @@ def convertEreaderToPml(infile, name, cc, outdir):
|
||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||
|
||||
|
||||
|
||||
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
outdir = tempfile.mkdtemp()
|
||||
try:
|
||||
print "Processing..."
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
if make_pmlz :
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
imageList = os.listdir(filePath)
|
||||
localimgdir = os.path.basename(filePath)
|
||||
for image in imageList:
|
||||
localname = os.path.join(localimgdir,image)
|
||||
imagePath = os.path.join(filePath,image)
|
||||
if os.path.isfile(imagePath):
|
||||
myZipFile.write(imagePath, localname)
|
||||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print 'output is %s' % zipname
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print "Converts DRMed eReader books to PML Source"
|
||||
print "Usage:"
|
||||
@@ -404,8 +467,8 @@ def usage():
|
||||
print " It's enough to enter the last 8 digits of the credit card number"
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
global bookname
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||
except getopt.GetoptError, err:
|
||||
@@ -413,76 +476,29 @@ def main(argv=None):
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
zipname = None
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
zipname = ''
|
||||
|
||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
else:
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
outdir = tempfile.mkdtemp()
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
import time
|
||||
start_time = time.time()
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||
|
||||
if make_pmlz :
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
imageList = os.listdir(filePath)
|
||||
localimgdir = os.path.basename(filePath)
|
||||
for image in imageList:
|
||||
localname = os.path.join(localimgdir,image)
|
||||
imagePath = os.path.join(filePath,image)
|
||||
if os.path.isfile(imagePath):
|
||||
myZipFile.write(imagePath, localname)
|
||||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
|
||||
end_time = time.time()
|
||||
search_time = end_time - start_time
|
||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
||||
if make_pmlz :
|
||||
print 'output is %s' % zipname
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.6.44 or higher.
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to Calibre a plugin.
|
||||
@@ -46,6 +46,9 @@
|
||||
# - Incorporated SomeUpdates zipfix routine.
|
||||
# 0.1.2 - bug fix for non-ascii file names in encryption.xml
|
||||
# 0.1.3 - Try PyCrypto on Windows first
|
||||
# 0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||
# 0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||
# 0.1.6 - update ot the new calibre plugin interface
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
@@ -264,6 +267,7 @@ def plugin_main(userkey, inpath, outpath):
|
||||
return 0
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
class IgnobleDeDRM(FileTypePlugin):
|
||||
name = 'Ignoble Epub DeDRM'
|
||||
@@ -271,8 +275,8 @@ class IgnobleDeDRM(FileTypePlugin):
|
||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
version = (0, 1, 3)
|
||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
||||
version = (0, 1, 6)
|
||||
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub'])
|
||||
on_import = True
|
||||
|
||||
@@ -280,10 +284,6 @@ class IgnobleDeDRM(FileTypePlugin):
|
||||
global AES
|
||||
global AES2
|
||||
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
AES, AES2 = _load_crypto()
|
||||
|
||||
if AES == None or AES2 == None:
|
||||
@@ -339,7 +339,7 @@ class IgnobleDeDRM(FileTypePlugin):
|
||||
for userkey in userkeys:
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
import zipfix
|
||||
from calibre_plugins.ignobleepub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
try:
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
|
||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class fixZip:
|
||||
def __init__(self, zinput, zoutput):
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
@@ -82,22 +93,28 @@ class fixZip:
|
||||
# and copy member over to output archive
|
||||
# if problems exist with local vs central filename, fix them
|
||||
|
||||
for i, zinfo in enumerate(self.inzip.infolist()):
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||
|
||||
try:
|
||||
data = self.inzip.read(zinfo)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zinfo.compress_type
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zinfo.compress_type
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
|
||||
self.bzf.close()
|
||||
self.inzip.close()
|
||||
@@ -111,14 +128,7 @@ def usage():
|
||||
"""
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv)!=3:
|
||||
usage()
|
||||
return 1
|
||||
infile = None
|
||||
outfile = None
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
def repairBook(infile, outfile):
|
||||
if not os.path.exists(infile):
|
||||
print "Error: Input Zip File does not exist"
|
||||
return 1
|
||||
@@ -130,6 +140,16 @@ def main(argv=sys.argv):
|
||||
print "Error Occurred ", e
|
||||
return 2
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv)!=3:
|
||||
usage()
|
||||
return 1
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
return repairBook(infile, outfile)
|
||||
|
||||
|
||||
if __name__ == '__main__' :
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Requires Calibre version 0.6.44 or higher.
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a Calibre plugin.
|
||||
@@ -47,7 +47,9 @@
|
||||
# result of Calibre changing to python 2.7.
|
||||
# 0.1.3 - bug fix for epubs with non-ascii chars in file names
|
||||
# 0.1.4 - default to try PyCrypto first on Windows
|
||||
|
||||
# 0.1.5 - update zipfix to handle out of position mimetypes
|
||||
# 0.1.6 - update zipfix to handle completely missing mimetype files
|
||||
# 0.1.7 - update to new calibre plugin interface
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
@@ -364,6 +366,7 @@ def plugin_main(userkey, inpath, outpath):
|
||||
return 0
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
class IneptDeDRM(FileTypePlugin):
|
||||
name = 'Inept Epub DeDRM'
|
||||
@@ -371,8 +374,8 @@ class IneptDeDRM(FileTypePlugin):
|
||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
version = (0, 1, 4)
|
||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
||||
version = (0, 1, 7)
|
||||
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub'])
|
||||
on_import = True
|
||||
priority = 100
|
||||
@@ -381,10 +384,6 @@ class IneptDeDRM(FileTypePlugin):
|
||||
global AES
|
||||
global RSA
|
||||
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
if AES == None or RSA == None:
|
||||
@@ -417,7 +416,7 @@ class IneptDeDRM(FileTypePlugin):
|
||||
# Calibre's configuration directory for future use.
|
||||
if iswindows or isosx:
|
||||
# ADE key retrieval script included in respective OS folder.
|
||||
from ade_key import retrieve_key
|
||||
from calibre_plugins.ineptepub.ade_key import retrieve_key
|
||||
try:
|
||||
keydata = retrieve_key()
|
||||
userkeys.append(keydata)
|
||||
@@ -438,7 +437,7 @@ class IneptDeDRM(FileTypePlugin):
|
||||
for userkey in userkeys:
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
import zipfix
|
||||
from calibre_plugins.ineptepub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
try:
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class fixZip:
|
||||
def __init__(self, zinput, zoutput):
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
@@ -82,22 +93,28 @@ class fixZip:
|
||||
# and copy member over to output archive
|
||||
# if problems exist with local vs central filename, fix them
|
||||
|
||||
for i, zinfo in enumerate(self.inzip.infolist()):
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||
|
||||
try:
|
||||
data = self.inzip.read(zinfo)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zinfo.compress_type
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zinfo.compress_type
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
|
||||
self.bzf.close()
|
||||
self.inzip.close()
|
||||
@@ -111,14 +128,7 @@ def usage():
|
||||
"""
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv)!=3:
|
||||
usage()
|
||||
return 1
|
||||
infile = None
|
||||
outfile = None
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
def repairBook(infile, outfile):
|
||||
if not os.path.exists(infile):
|
||||
print "Error: Input Zip File does not exist"
|
||||
return 1
|
||||
@@ -130,6 +140,16 @@ def main(argv=sys.argv):
|
||||
print "Error Occurred ", e
|
||||
return 2
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv)!=3:
|
||||
usage()
|
||||
return 1
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
return repairBook(infile, outfile)
|
||||
|
||||
|
||||
if __name__ == '__main__' :
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
# ineptpdf_plugin.py
|
||||
# ineptpdf plugin __init__.py
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# Requires Calibre version 0.6.44 or higher.
|
||||
# Requires Calibre version 0.7.55 or higher.
|
||||
#
|
||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||
# I had the much easier job of converting them to a Calibre plugin.
|
||||
@@ -47,7 +47,11 @@
|
||||
# ** NOTE ** There is no plugin customization data for the Inept PDF DeDRM plugin.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.1 - Initial release
|
||||
# 0.1 - Initial release
|
||||
# 0.1.1 - back port ineptpdf 8.4.X support for increased number of encryption methods
|
||||
# 0.1.2 - back port ineptpdf 8.4.X bug fixes
|
||||
# 0.1.3 - add in fix for improper rejection of session bookkeys with len(bookkey) = length + 1
|
||||
# 0.1.4 - update to the new calibre plugin interface
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
@@ -171,6 +175,7 @@ def _load_crypto_libcrypto():
|
||||
return out.raw
|
||||
|
||||
class AES(object):
|
||||
MODE_CBC = 0
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
self = AES()
|
||||
@@ -1544,16 +1549,30 @@ class PDFDocument(object):
|
||||
bookkey = bookkey[index:]
|
||||
ebx_V = int_value(param.get('V', 4))
|
||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||
# added because of the booktype / decryption book session key error
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
elif ebx_V < 4 or ebx_type < 6:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
# added because of improper booktype / decryption book session key errors
|
||||
if length > 0:
|
||||
if len(bookkey) == length:
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
elif len(bookkey) == length + 1:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
else:
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||
else:
|
||||
V = 2
|
||||
if length and len(bookkey) != length:
|
||||
raise ADEPTError('error decrypting book session key')
|
||||
# proper length unknown try with whatever you have
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
self.decrypt_key = bookkey
|
||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||
self.decipher = self.decrypt_rc4
|
||||
@@ -2109,6 +2128,7 @@ def plugin_main(keypath, inpath, outpath):
|
||||
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
class IneptPDFDeDRM(FileTypePlugin):
|
||||
name = 'Inept PDF DeDRM'
|
||||
@@ -2116,17 +2136,14 @@ class IneptPDFDeDRM(FileTypePlugin):
|
||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
version = (0, 1, 2)
|
||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
||||
version = (0, 1, 4)
|
||||
minimum_calibre_version = (0, 7, 55) # for the new plugin interface
|
||||
file_types = set(['pdf'])
|
||||
on_import = True
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
global ARC4, RSA, AES
|
||||
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
ARC4, RSA, AES = _load_crypto()
|
||||
|
||||
@@ -2160,7 +2177,7 @@ class IneptPDFDeDRM(FileTypePlugin):
|
||||
# Calibre's configuration directory for future use.
|
||||
if iswindows or isosx:
|
||||
# ADE key retrieval script.
|
||||
from ade_key import retrieve_key
|
||||
from calibre_plugins.ineptpdf.ade_key import retrieve_key
|
||||
try:
|
||||
keydata = retrieve_key()
|
||||
userkeys.append(keydata)
|
||||
Binary file not shown.
@@ -1,367 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
||||
#
|
||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
||||
# for the plugin version to function properly.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||
|
||||
|
||||
__version__ = '2.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
tempdir = tempfile.mkdtemp()
|
||||
mb = topazextract.TopazBook(infile, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = bookname
|
||||
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||
outfilename = outfilename + "_" + filenametitle
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
if not mobi:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 1
|
||||
pass
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi')
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 0
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
if not __name__ == "__main__" and inCalibre:
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 2, 1) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
import kgenpids
|
||||
import zlib
|
||||
import zipfile
|
||||
import topazextract
|
||||
import mobidedrm
|
||||
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
||||
pass
|
||||
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
tempdir = PersistentTemporaryDirectory()
|
||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
of = self.temporary_file(bookname + '.zip')
|
||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip, tempdir, 'img')
|
||||
myzip.close()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
||||
@@ -1,10 +1,12 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -66,9 +68,8 @@ def _load_crypto_libcrypto():
|
||||
raise DrmException('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd):
|
||||
salt = '16743'
|
||||
saltlen = 5
|
||||
def keyivgen(self, passwd, salt):
|
||||
saltlen = len(salt)
|
||||
passlen = len(passwd)
|
||||
iter = 0x3e8
|
||||
keylen = 80
|
||||
@@ -91,12 +92,78 @@ LibCrypto = _load_crypto()
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
# generate table of prime number less than or equal to int n
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
|
||||
@@ -137,30 +204,12 @@ def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
import hashlib
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
def CryptUnprotectData(encryptedData):
|
||||
def CryptUnprotectData(encryptedData, salt):
|
||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
crp = LibCrypto()
|
||||
key_iv = crp.keyivgen(passwdData)
|
||||
key_iv = crp.keyivgen(passwdData, salt)
|
||||
key = key_iv[0:32]
|
||||
iv = key_iv[32:48]
|
||||
crp.set_decrypt_key(key,iv)
|
||||
@@ -168,27 +217,141 @@ def CryptUnprotectData(encryptedData):
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate and open the .kindle-info file
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('.kindle-info')
|
||||
if pp >= 0:
|
||||
kinfopath = resline
|
||||
break
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: .kindle-info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
else:
|
||||
if not os.path.isfile(kinfoFile):
|
||||
raise DrmException('Error: kindle-info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
# Locate the .kindle-info files
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
# first search for current .kindle-info files
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
found = False
|
||||
for resline in reslst:
|
||||
if os.path.isfile(resline):
|
||||
kInfoFiles.append(resline)
|
||||
found = True
|
||||
# For Future Reference
|
||||
#
|
||||
# # add any .kinf files
|
||||
# cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"'
|
||||
# cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
# p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
# out1, out2 = p1.communicate()
|
||||
# reslst = out1.split('\n')
|
||||
# for resline in reslst:
|
||||
# if os.path.isfile(resline):
|
||||
# kInfoFiles.append(resline)
|
||||
# found = True
|
||||
if not found:
|
||||
print('No kindle-info files have been found.')
|
||||
return kInfoFiles
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('[') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
salt = '16743'
|
||||
cleartext = CryptUnprotectData(encryptedValue, salt)
|
||||
DB[keyname] = decode(cleartext,charMap1)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# For Future Reference taken from K4PC 1.5.0 .kinf
|
||||
#
|
||||
# # else newer 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('/')
|
||||
#
|
||||
# # loop through the item records until all are processed
|
||||
# while len(items) > 0:
|
||||
#
|
||||
# # get the first item record
|
||||
# item = items.pop(0)
|
||||
#
|
||||
# # the first 32 chars of the first record of a group
|
||||
# # is the MD5 hash of the key name encoded by charMap5
|
||||
# keyhash = item[0:32]
|
||||
#
|
||||
# # the raw keyhash string is also used to create entropy for the actual
|
||||
# # CryptProtectData Blob that represents that keys contents
|
||||
# entropy = SHA1(keyhash)
|
||||
#
|
||||
# # the remainder of the first record when decoded with charMap5
|
||||
# # has the ':' split char followed by the string representation
|
||||
# # of the number of records that follow
|
||||
# # and make up the contents
|
||||
# srcnt = decode(item[34:],charMap5)
|
||||
# rcnt = int(srcnt)
|
||||
#
|
||||
# # read and store in rcnt records of data
|
||||
# # that make up the contents value
|
||||
# edlst = []
|
||||
# for i in xrange(rcnt):
|
||||
# item = items.pop(0)
|
||||
# edlst.append(item)
|
||||
#
|
||||
# keyname = "unknown"
|
||||
# for name in names:
|
||||
# if encodeHash(name,charMap5) == keyhash:
|
||||
# keyname = name
|
||||
# break
|
||||
# if keyname == "unknown":
|
||||
# keyname = keyhash
|
||||
#
|
||||
# # the charMap5 encoded contents data has had a length
|
||||
# # of chars (always odd) cut off of the front and moved
|
||||
# # to the end to prevent decoding using charMap5 from
|
||||
# # working properly, and thereby preventing the ensuing
|
||||
# # CryptUnprotectData call from succeeding.
|
||||
#
|
||||
# # The offset into the charMap5 encoded contents seems to be:
|
||||
# # len(contents) - largest prime number 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)
|
||||
# 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)
|
||||
# cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys, os
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
@@ -10,25 +12,86 @@ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
|
||||
import _winreg as winreg
|
||||
|
||||
import traceback
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
import traceback
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
|
||||
# simple primes table (<= n) calculator
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
# Various character maps used to decrypt kindle info values.
|
||||
# Probably supposed to act as obfuscation
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# interface with Windows OS Routines
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
@@ -59,52 +122,175 @@ def GetVolumeSerialNumber():
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
def getLastError():
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetLastError.argtypes = None
|
||||
GetLastError.restype = c_uint
|
||||
def getLastError():
|
||||
return GetLastError()
|
||||
return getLastError
|
||||
getLastError = getLastError()
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
buffer = create_unicode_buffer(2)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
errcd = getLastError()
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
def CryptUnprotectData(indata, entropy, flags):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
None, None, flags, byref(outdata)):
|
||||
raise DrmException("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
#
|
||||
# Locate and open the Kindle.info file.
|
||||
#
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
|
||||
# Locate all of the kindle-info style files and return as list
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
|
||||
# first look for older kindle-info files
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No kindle.info files have not been found.')
|
||||
else:
|
||||
if not os.path.isfile(kInfoFile):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
kInfoFiles.append(kinfopath)
|
||||
|
||||
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No .kinf files have not been found.')
|
||||
else:
|
||||
kInfoFiles.append(kinfopath)
|
||||
return kInfoFiles
|
||||
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('{') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# else newer 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('/')
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number 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)
|
||||
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)
|
||||
cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
|
||||
|
||||
@@ -46,8 +46,14 @@
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They aren't for DOC compressed
|
||||
# files, but they are for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
|
||||
__version__ = '0.26'
|
||||
__version__ = '0.30'
|
||||
|
||||
import sys
|
||||
|
||||
@@ -158,6 +164,7 @@ class MobiBook:
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
@@ -175,6 +182,7 @@ class MobiBook:
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
@@ -190,7 +198,7 @@ class MobiBook:
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
if (self.mobi_version < 7) and (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
@@ -207,19 +215,16 @@ class MobiBook:
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
@@ -239,18 +244,18 @@ class MobiBook:
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
rec209 = ''
|
||||
token = ''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in xrange(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
@@ -298,13 +303,17 @@ class MobiBook:
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
@@ -350,33 +359,35 @@ class MobiBook:
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
self.mobi_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
print "done"
|
||||
return self.data_file
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
|
||||
Binary file not shown.
@@ -24,7 +24,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>DeDRM 2.0, Copyright © 2010–2011 by Apprentice Alf.</string>
|
||||
<string>DeDRM 2.7, Written 2010–2011 by Apprentice Alf and others.</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
@@ -34,7 +34,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0</string>
|
||||
<string>2.7</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>dplt</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
@@ -43,14 +43,18 @@
|
||||
<true/>
|
||||
<key>WindowState</key>
|
||||
<dict>
|
||||
<key>dividerCollapsed</key>
|
||||
<true/>
|
||||
<key>eventLogLevel</key>
|
||||
<integer>-1</integer>
|
||||
<key>name</key>
|
||||
<string>ScriptWindowState</string>
|
||||
<key>positionOfDivider</key>
|
||||
<real>709</real>
|
||||
<real>0</real>
|
||||
<key>savedFrame</key>
|
||||
<string>1617 62 862 788 1440 -150 1680 1050 </string>
|
||||
<string>1578 27 862 788 1440 -150 1680 1050 </string>
|
||||
<key>selectedTabView</key>
|
||||
<string>result</string>
|
||||
<string>event log</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350
|
||||
{\fonttbl}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 362 B |
@@ -58,8 +58,9 @@
|
||||
# 0.17 - added support for pycrypto's DES as well
|
||||
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||
# 0.19 - Modify the interface to allow use of import
|
||||
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||
|
||||
__version__='0.19'
|
||||
__version__='0.20'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -71,32 +72,50 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
# first try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
if Des == None:
|
||||
# they try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
else:
|
||||
# first try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
if Des == None:
|
||||
# then try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
|
||||
# if that did not work then use pure python implementation
|
||||
# of DES and try to speed it up with Psycho
|
||||
if Des == None:
|
||||
import python_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import python_des
|
||||
else:
|
||||
import python_des
|
||||
Des = python_des.Des
|
||||
# Import Psyco if available
|
||||
try:
|
||||
@@ -480,5 +499,6 @@ def main(argv=None):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
||||
ys = []
|
||||
gdefs = []
|
||||
|
||||
# get path defintions, positions, dimensions for ecah glyph
|
||||
# get path defintions, positions, dimensions for each glyph
|
||||
# that makes up the image, and find min x and min y to reposition origin
|
||||
minx = -1
|
||||
miny = -1
|
||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
||||
lastGlyph = firstglyphList[last]
|
||||
else :
|
||||
lastGlyph = len(gidList)
|
||||
|
||||
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||
# by reverting to text based paragraph
|
||||
if firstGlyph >= lastGlyph:
|
||||
# revert to standard text based paragraph
|
||||
for wordnum in xrange(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||
glyphList.append(glyphnum)
|
||||
# include any extratokens if they exist
|
||||
|
||||
@@ -21,10 +21,21 @@ from struct import unpack
|
||||
|
||||
|
||||
# local support routines
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.k4mobidedrm import convert2xml
|
||||
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
|
||||
# Get a 7 bit encoded number from a file
|
||||
@@ -192,6 +203,8 @@ class GParser(object):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
def getGlyphDim(self, gly):
|
||||
if self.gdpi[gly] == 0:
|
||||
return 0, 0
|
||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||
return maxh, maxw
|
||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
print 'Processing Meta Data and creating OPF'
|
||||
meta_array = getMetaArray(metaFile)
|
||||
|
||||
# replace special chars in title and authors like & < >
|
||||
title = meta_array.get('Title','No Title Provided')
|
||||
title = title.replace('&','&')
|
||||
title = title.replace('<','<')
|
||||
title = title.replace('>','>')
|
||||
meta_array['Title'] = title
|
||||
authors = meta_array.get('Authors','No Authors Provided')
|
||||
authors = authors.replace('&','&')
|
||||
authors = authors.replace('<','<')
|
||||
authors = authors.replace('>','>')
|
||||
meta_array['Authors'] = authors
|
||||
|
||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||
metastr = ''
|
||||
for key in meta_array:
|
||||
@@ -399,8 +424,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||
htmlstr += '</head>\n<body>\n'
|
||||
|
||||
@@ -416,8 +443,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
svgindex += '</head>\n'
|
||||
svgindex += '<body>\n'
|
||||
|
||||
@@ -471,9 +500,12 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||
# adding metadata
|
||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
if 'GUID' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
if 'ASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
if 'oASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||
opfstr += ' <dc:language>en</dc:language>\n'
|
||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += ' </metadata>\n'
|
||||
opfstr += '<manifest>\n'
|
||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||
# adding image files to manifest
|
||||
filenames = os.listdir(imgDir)
|
||||
filenames = sorted(filenames)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ignobleepub.pyw, version 3.3
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.4
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
@@ -14,10 +16,9 @@
|
||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 3.4 - Modify interace to allow use with import
|
||||
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
@@ -170,49 +171,6 @@ class Decryptor(object):
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
# aes = AES.new(key, AES.MODE_CBC)
|
||||
aes = AES(key)
|
||||
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
@@ -308,6 +266,53 @@ class DecryptionDialog(Tkinter.Frame):
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
# aes = AES.new(key, AES.MODE_CBC)
|
||||
aes = AES(key)
|
||||
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
@@ -324,6 +329,7 @@ def gui_main():
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
@@ -1,6 +1,8 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ignoblekeygen.pyw, version 2.2
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignoblekeygen.pyw, version 2.3
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
@@ -12,12 +14,12 @@
|
||||
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||
# 2.3 - Modify interface to allow use of import
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
@@ -120,6 +122,7 @@ AES = _load_crypto()
|
||||
def normalize_name(name):
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = ccn + '\x00'
|
||||
@@ -133,19 +136,6 @@ def generate_keyfile(name, ccn, outpath):
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
return 0
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
@@ -211,6 +201,22 @@ class DecryptionDialog(Tkinter.Frame):
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
return 0
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
@@ -1,7 +1,9 @@
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ineptepub.pyw, version 5.5
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
@@ -27,13 +29,11 @@
|
||||
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
@@ -312,45 +312,6 @@ class Decryptor(object):
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
@@ -446,6 +407,52 @@ class DecryptionDialog(Tkinter.Frame):
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
@@ -1,7 +1,9 @@
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ineptkey.pyw, version 5.3
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptkey.pyw, version 5.4
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
@@ -33,13 +35,12 @@
|
||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||
# 5.2 - added support for output of key to a particular file
|
||||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
@@ -415,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame):
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
keypath = argv[1]
|
||||
|
||||
def extractKeyfile(keypath):
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
@@ -431,6 +433,12 @@ def cli_main(argv=sys.argv):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
@@ -1,5 +1,7 @@
|
||||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.7
|
||||
# ineptpdf.pyw, version 7.9
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
||||
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||
@@ -30,13 +32,13 @@
|
||||
# fixed minor typos
|
||||
# 7.6 - backported AES and other fixes from version 8.4.48
|
||||
# 7.7 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 7.8 - Modify interface to allow use of import
|
||||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
@@ -155,6 +157,7 @@ def _load_crypto_libcrypto():
|
||||
return out.raw
|
||||
|
||||
class AES(object):
|
||||
MODE_CBC = 0
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
self = AES()
|
||||
@@ -1530,16 +1533,30 @@ class PDFDocument(object):
|
||||
bookkey = bookkey[index:]
|
||||
ebx_V = int_value(param.get('V', 4))
|
||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||
# added because of the booktype / decryption book session key error
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
elif ebx_V < 4 or ebx_type < 6:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
# added because of improper booktype / decryption book session key errors
|
||||
if length > 0:
|
||||
if len(bookkey) == length:
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
elif len(bookkey) == length + 1:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
else:
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||
else:
|
||||
V = 2
|
||||
if length and len(bookkey) != length:
|
||||
raise ADEPTError('error decrypting book session key')
|
||||
# proper length unknown try with whatever you have
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
self.decrypt_key = bookkey
|
||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||
self.decipher = self.decrypt_rc4
|
||||
@@ -2076,25 +2093,6 @@ class PDFSerializer(object):
|
||||
self.write('\n')
|
||||
self.write('endobj\n')
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
with open(inpath, 'rb') as inf:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
serializer.dump(outf)
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
@@ -2198,6 +2196,31 @@ class DecryptionDialog(Tkinter.Frame):
|
||||
'Close this window or decrypt another pdf file.'
|
||||
return
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
with open(inpath, 'rb') as inf:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
serializer.dump(outf)
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if RSA is None:
|
||||
@@ -16,20 +16,8 @@ from __future__ import with_statement
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
||||
#
|
||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
||||
# for the plugin version to function properly.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||
|
||||
|
||||
__version__ = '2.1'
|
||||
__version__ = '3.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -43,11 +31,7 @@ class Unbuffered:
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -57,45 +41,39 @@ if 'calibre' in sys.modules:
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert spaces to underscores
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
return one
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace(' ','_')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
@@ -111,73 +89,53 @@ def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
tempdir = tempfile.mkdtemp()
|
||||
mb = topazextract.TopazBook(infile, tempdir)
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = bookname
|
||||
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||
outfilename = outfilename + "_"+filenametitle
|
||||
outfilename = outfilename + "_" + filenametitle
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi')
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
return 0
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
print " Creating SVG HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
mb.getXMLZip(zipname)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -198,7 +156,7 @@ def main(argv=sys.argv):
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
@@ -227,9 +185,11 @@ def main(argv=sys.argv):
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
@@ -237,129 +197,3 @@ if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
if not __name__ == "__main__" and inCalibre:
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 2, 1) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
import kgenpids
|
||||
import zlib
|
||||
import zipfile
|
||||
import topazextract
|
||||
import mobidedrm
|
||||
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
||||
pass
|
||||
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
tempdir = PersistentTemporaryDirectory()
|
||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
of = self.temporary_file(bookname + '.zip')
|
||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip, tempdir, 'img')
|
||||
myzip.close()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -66,9 +68,8 @@ def _load_crypto_libcrypto():
|
||||
raise DrmException('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd):
|
||||
salt = '16743'
|
||||
saltlen = 5
|
||||
def keyivgen(self, passwd, salt):
|
||||
saltlen = len(salt)
|
||||
passlen = len(passwd)
|
||||
iter = 0x3e8
|
||||
keylen = 80
|
||||
@@ -91,12 +92,78 @@ LibCrypto = _load_crypto()
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
# generate table of prime number less than or equal to int n
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
|
||||
@@ -137,30 +204,12 @@ def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
import hashlib
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
def CryptUnprotectData(encryptedData):
|
||||
def CryptUnprotectData(encryptedData, salt):
|
||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
crp = LibCrypto()
|
||||
key_iv = crp.keyivgen(passwdData)
|
||||
key_iv = crp.keyivgen(passwdData, salt)
|
||||
key = key_iv[0:32]
|
||||
iv = key_iv[32:48]
|
||||
crp.set_decrypt_key(key,iv)
|
||||
@@ -168,27 +217,141 @@ def CryptUnprotectData(encryptedData):
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate and open the .kindle-info file
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('.kindle-info')
|
||||
if pp >= 0:
|
||||
kinfopath = resline
|
||||
break
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: .kindle-info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
else:
|
||||
if not os.path.isfile(kinfoFile):
|
||||
raise DrmException('Error: kindle-info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
# Locate the .kindle-info files
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
# first search for current .kindle-info files
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
found = False
|
||||
for resline in reslst:
|
||||
if os.path.isfile(resline):
|
||||
kInfoFiles.append(resline)
|
||||
found = True
|
||||
# For Future Reference
|
||||
#
|
||||
# # add any .kinf files
|
||||
# cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"'
|
||||
# cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
# p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
# out1, out2 = p1.communicate()
|
||||
# reslst = out1.split('\n')
|
||||
# for resline in reslst:
|
||||
# if os.path.isfile(resline):
|
||||
# kInfoFiles.append(resline)
|
||||
# found = True
|
||||
if not found:
|
||||
print('No kindle-info files have been found.')
|
||||
return kInfoFiles
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('[') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
salt = '16743'
|
||||
cleartext = CryptUnprotectData(encryptedValue, salt)
|
||||
DB[keyname] = decode(cleartext,charMap1)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# For Future Reference taken from K4PC 1.5.0 .kinf
|
||||
#
|
||||
# # else newer 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('/')
|
||||
#
|
||||
# # loop through the item records until all are processed
|
||||
# while len(items) > 0:
|
||||
#
|
||||
# # get the first item record
|
||||
# item = items.pop(0)
|
||||
#
|
||||
# # the first 32 chars of the first record of a group
|
||||
# # is the MD5 hash of the key name encoded by charMap5
|
||||
# keyhash = item[0:32]
|
||||
#
|
||||
# # the raw keyhash string is also used to create entropy for the actual
|
||||
# # CryptProtectData Blob that represents that keys contents
|
||||
# entropy = SHA1(keyhash)
|
||||
#
|
||||
# # the remainder of the first record when decoded with charMap5
|
||||
# # has the ':' split char followed by the string representation
|
||||
# # of the number of records that follow
|
||||
# # and make up the contents
|
||||
# srcnt = decode(item[34:],charMap5)
|
||||
# rcnt = int(srcnt)
|
||||
#
|
||||
# # read and store in rcnt records of data
|
||||
# # that make up the contents value
|
||||
# edlst = []
|
||||
# for i in xrange(rcnt):
|
||||
# item = items.pop(0)
|
||||
# edlst.append(item)
|
||||
#
|
||||
# keyname = "unknown"
|
||||
# for name in names:
|
||||
# if encodeHash(name,charMap5) == keyhash:
|
||||
# keyname = name
|
||||
# break
|
||||
# if keyname == "unknown":
|
||||
# keyname = keyhash
|
||||
#
|
||||
# # the charMap5 encoded contents data has had a length
|
||||
# # of chars (always odd) cut off of the front and moved
|
||||
# # to the end to prevent decoding using charMap5 from
|
||||
# # working properly, and thereby preventing the ensuing
|
||||
# # CryptUnprotectData call from succeeding.
|
||||
#
|
||||
# # The offset into the charMap5 encoded contents seems to be:
|
||||
# # len(contents) - largest prime number 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)
|
||||
# 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)
|
||||
# cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys, os
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
@@ -10,25 +12,86 @@ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
|
||||
import _winreg as winreg
|
||||
|
||||
import traceback
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
import traceback
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
|
||||
# simple primes table (<= n) calculator
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
# Various character maps used to decrypt kindle info values.
|
||||
# Probably supposed to act as obfuscation
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# interface with Windows OS Routines
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
@@ -59,52 +122,175 @@ def GetVolumeSerialNumber():
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
def getLastError():
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetLastError.argtypes = None
|
||||
GetLastError.restype = c_uint
|
||||
def getLastError():
|
||||
return GetLastError()
|
||||
return getLastError
|
||||
getLastError = getLastError()
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
buffer = create_unicode_buffer(2)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
errcd = getLastError()
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
def CryptUnprotectData(indata, entropy, flags):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
None, None, flags, byref(outdata)):
|
||||
raise DrmException("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
#
|
||||
# Locate and open the Kindle.info file.
|
||||
#
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
|
||||
# Locate all of the kindle-info style files and return as list
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
|
||||
# first look for older kindle-info files
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No kindle.info files have not been found.')
|
||||
else:
|
||||
if not os.path.isfile(kInfoFile):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
kInfoFiles.append(kinfopath)
|
||||
|
||||
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No .kinf files have not been found.')
|
||||
else:
|
||||
kInfoFiles.append(kinfopath)
|
||||
return kInfoFiles
|
||||
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('{') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# else newer 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('/')
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number 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)
|
||||
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)
|
||||
cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
|
||||
|
||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
global charMap3
|
||||
global charMap4
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
@@ -66,66 +78,7 @@ def decode(data,map):
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
def parseKindleInfo(kInfoFile):
|
||||
DB = {}
|
||||
infoReader = openKindleInfo(kInfoFile)
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
if sys.platform.startswith('win'):
|
||||
items = data.split('{')
|
||||
else :
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
if sys.platform.startswith('win'):
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
else:
|
||||
cleartext = CryptUnprotectData(encryptedValue)
|
||||
return decode(cleartext, charMap1)
|
||||
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForKey(key):
|
||||
global charMap2
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
# Find if the original string for a hashed/encoded string is known.
|
||||
# If so return the original string othwise return an empty string.
|
||||
def findNameForHash(hash):
|
||||
global charMap2
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return result
|
||||
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
@@ -224,13 +177,11 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
|
||||
if rec209 != None and token != None:
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
@@ -240,15 +191,15 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
return pidlst
|
||||
|
||||
|
||||
# Parse the EXTH header records and parse the Kindleinfo
|
||||
# file to calculate the book pid.
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
global kindleDatabase
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
print(message)
|
||||
kindleDatabase = None
|
||||
@@ -256,10 +207,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
||||
|
||||
@@ -275,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
|
||||
# Compute book PID
|
||||
if rec209 == None or token == None:
|
||||
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
|
||||
return pidlst
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
# Compute book PIDs
|
||||
|
||||
# book pid
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
@@ -305,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
pidlst = getK4Pids(pidlst, md1, md2)
|
||||
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||
for infoFile in kInfoFiles:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
for serialnum in serials:
|
||||
|
||||
@@ -46,8 +46,14 @@
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They aren't for DOC compressed
|
||||
# files, but they are for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
|
||||
__version__ = '0.26'
|
||||
__version__ = '0.30'
|
||||
|
||||
import sys
|
||||
|
||||
@@ -158,6 +164,7 @@ class MobiBook:
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
@@ -175,6 +182,7 @@ class MobiBook:
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
@@ -190,7 +198,7 @@ class MobiBook:
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
if (self.mobi_version < 7) and (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
@@ -207,19 +215,16 @@ class MobiBook:
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
@@ -239,18 +244,18 @@ class MobiBook:
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
rec209 = ''
|
||||
token = ''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in xrange(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
@@ -298,13 +303,17 @@ class MobiBook:
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
@@ -350,33 +359,35 @@ class MobiBook:
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
self.mobi_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
print "done"
|
||||
return self.data_file
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
|
||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
@@ -18,10 +23,32 @@ from struct import unpack
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
import kgenpids
|
||||
import genbook
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
import kgenpids
|
||||
import genbook
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tdir, localfilePath)
|
||||
|
||||
#
|
||||
# Utility routines
|
||||
#
|
||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
||||
|
||||
|
||||
class TopazBook:
|
||||
def __init__(self, filename, outdir):
|
||||
def __init__(self, filename):
|
||||
self.fo = file(filename, 'rb')
|
||||
self.outdir = outdir
|
||||
self.outdir = tempfile.mkdtemp()
|
||||
self.bookPayloadOffset = 0
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
@@ -157,18 +184,22 @@ class TopazBook:
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
for i in range (0,nbRecords) :
|
||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
||||
self.bookMetadata[record[0]] = record[1]
|
||||
keyval = bookReadString(self.fo)
|
||||
content = bookReadString(self.fo)
|
||||
# print keyval
|
||||
# print content
|
||||
self.bookMetadata[keyval] = content
|
||||
return self.bookMetadata
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
keysRecord = None
|
||||
keysRecordRecord = None
|
||||
if 'keys' in self.bookMetadata:
|
||||
keysRecord = self.bookMetadata['keys']
|
||||
if keysRecord in self.bookMetadata:
|
||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
||||
keysRecord = self.bookMetadata.get('keys','')
|
||||
keysRecordRecord = ''
|
||||
if keysRecord != '':
|
||||
keylst = keysRecord.split(',')
|
||||
for keyval in keylst:
|
||||
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||
return keysRecord, keysRecordRecord
|
||||
|
||||
def getBookTitle(self):
|
||||
@@ -313,21 +344,33 @@ class TopazBook:
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.close()
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
@@ -379,58 +422,46 @@ def main(argv=sys.argv):
|
||||
return 1
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
tb = TopazBook(infile, tempdir)
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
tb.processBook(pidlst)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
tb.cleanup()
|
||||
return 1
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
tb.cleanup
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class fixZip:
|
||||
def __init__(self, zinput, zoutput):
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
@@ -81,30 +92,15 @@ class fixZip:
|
||||
# get the zipinfo for each member of the input archive
|
||||
# and copy member over to output archive
|
||||
# if problems exist with local vs central filename, fix them
|
||||
# also fix bad epub compression
|
||||
|
||||
# write mimetype file first, if present, and with no compression
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename == "mimetype":
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zipfile.ZIP_STORED
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
nzinfo.extra = ""
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
break
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != "mimetype":
|
||||
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path
|
||||
sys.path.append(sys.path[0]+os.sep+'lib')
|
||||
import shutil
|
||||
import Tkinter
|
||||
from Tkinter import *
|
||||
@@ -22,10 +22,11 @@ class DrmException(Exception):
|
||||
pass
|
||||
|
||||
class MainApp(Tk):
|
||||
def __init__(self, dnd=False, filenames=[]):
|
||||
def __init__(self, apphome, dnd=False, filenames=[]):
|
||||
Tk.__init__(self)
|
||||
self.withdraw()
|
||||
self.dnd = dnd
|
||||
self.apphome = apphome
|
||||
# preference settings
|
||||
# [dictionary key, file in preferences directory where info is stored]
|
||||
description = [ ['pids' , 'pidlist.txt' ],
|
||||
@@ -123,14 +124,17 @@ class PrefsDialog(Toplevel):
|
||||
button = Tkinter.Button(body, text="...", command=self.get_bnkpath)
|
||||
button.grid(row=1, column=2)
|
||||
|
||||
Tkinter.Label(body, text='Additional kindle.info file').grid(row=2, sticky=Tkconstants.E)
|
||||
Tkinter.Label(body, text='Additional kindle.info or .kinf file').grid(row=2, sticky=Tkconstants.E)
|
||||
self.altinfopath = Tkinter.Entry(body, width=50)
|
||||
self.altinfopath.grid(row=2, column=1, sticky=sticky)
|
||||
prefdir = self.prefs_array['dir']
|
||||
infofile = os.path.join(prefdir,'kindle.info')
|
||||
path = ''
|
||||
infofile = os.path.join(prefdir,'kindle.info')
|
||||
ainfofile = os.path.join(prefdir,'.kinf')
|
||||
if os.path.isfile(infofile):
|
||||
path = infofile
|
||||
elif os.path.isfile(ainfofile):
|
||||
path = ainfofile
|
||||
path = path.encode('utf-8')
|
||||
self.altinfopath.insert(0, path)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_altinfopath)
|
||||
@@ -244,8 +248,8 @@ class PrefsDialog(Toplevel):
|
||||
|
||||
def get_altinfopath(self):
|
||||
cpath = self.altinfopath.get()
|
||||
altinfopath = tkFileDialog.askopenfilename(parent=None, title='Select Alternative kindle.info File',
|
||||
defaultextension='.info', filetypes=[('Kindle Info', '.info'),('All Files', '.*')],
|
||||
altinfopath = tkFileDialog.askopenfilename(parent=None, title='Select Alternative kindle.info or .kinf File',
|
||||
defaultextension='.info', filetypes=[('Kindle Info', '.info'),('Kindle KInf','.kinf')('All Files', '.*')],
|
||||
initialdir=cpath)
|
||||
if altinfopath:
|
||||
altinfopath = os.path.normpath(altinfopath)
|
||||
@@ -312,6 +316,7 @@ class ConvDialog(Toplevel):
|
||||
self.protocol("WM_DELETE_WINDOW", self.withdraw)
|
||||
self.title("DeDRM Processing")
|
||||
self.master = master
|
||||
self.apphome = self.master.apphome
|
||||
self.prefs_array = prefs_array
|
||||
self.filenames = filenames
|
||||
self.interval = 50
|
||||
@@ -328,11 +333,11 @@ class ConvDialog(Toplevel):
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Activity Bar').grid(row=0, sticky=Tkconstants.E)
|
||||
self.bar = ActivityBar(body, length=50, height=15, barwidth=5)
|
||||
self.bar = ActivityBar(body, length=80, height=15, barwidth=5)
|
||||
self.bar.grid(row=0, column=1, sticky=sticky)
|
||||
|
||||
msg1 = ''
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=4, width=50, wrap=Tkconstants.WORD)
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=4, width=80, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
@@ -435,7 +440,6 @@ class ConvDialog(Toplevel):
|
||||
if poll != 0:
|
||||
msg = 'Failed\n'
|
||||
text = self.p2.read()
|
||||
text = self.p2.read()
|
||||
text += self.p2.readerr()
|
||||
msg += text
|
||||
msg += '\n'
|
||||
@@ -451,42 +455,41 @@ class ConvDialog(Toplevel):
|
||||
return
|
||||
|
||||
def decrypt_ebook(self, infile, outdir, rscpath):
|
||||
apphome = self.apphome
|
||||
rv = 1
|
||||
name, ext = os.path.splitext(os.path.basename(infile))
|
||||
ext = ext.lower()
|
||||
if ext == '.epub':
|
||||
outfile = os.path.join(outdir, name + '_nodrm.epub')
|
||||
self.p2 = processEPUB(infile, outfile, rscpath)
|
||||
self.p2 = processEPUB(apphome, infile, outdir, rscpath)
|
||||
return 0
|
||||
if ext == '.pdb':
|
||||
self.p2 = processPDB(infile, outdir, rscpath)
|
||||
self.p2 = processPDB(apphome, infile, outdir, rscpath)
|
||||
return 0
|
||||
if ext in ['.azw', '.azw1', '.prc', '.mobi', '.tpz']:
|
||||
self.p2 = processK4MOBI(infile, outdir, rscpath)
|
||||
self.p2 = processK4MOBI(apphome, infile, outdir, rscpath)
|
||||
return 0
|
||||
if ext == '.pdf':
|
||||
outfile = os.path.join(outdir, name + '_nodrm.pdf')
|
||||
self.p2 = processPDF(infile, outfile, rscpath)
|
||||
self.p2 = processPDF(apphome, infile, outdir, rscpath)
|
||||
return 0
|
||||
return rv
|
||||
|
||||
|
||||
# run as a subprocess via pipes and collect stdout, stderr, and return value
|
||||
def runit(ncmd, nparms):
|
||||
cmdline = 'python ' + ncmd
|
||||
def runit(apphome, ncmd, nparms):
|
||||
cmdline = 'python ' + '"' + os.path.join(apphome, ncmd) + '" '
|
||||
if sys.platform.startswith('win'):
|
||||
search_path = os.environ['PATH']
|
||||
search_path = search_path.lower()
|
||||
if search_path.find('python') < 0:
|
||||
# if no python hope that win registry finds what is associated with py extension
|
||||
cmdline = ncmd
|
||||
cmdline = '"' + os.path.join(apphome, ncmd) + '" '
|
||||
cmdline += nparms
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = subasyncio.Process(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
def processK4MOBI(infile, outdir, rscpath):
|
||||
cmd = '"' + os.path.join('lib','k4mobidedrm.py') + '" '
|
||||
def processK4MOBI(apphome, infile, outdir, rscpath):
|
||||
cmd = os.path.join('lib','k4mobidedrm.py')
|
||||
parms = ''
|
||||
pidnums = ''
|
||||
pidspath = os.path.join(rscpath,'pidlist.txt')
|
||||
@@ -504,37 +507,40 @@ def processK4MOBI(infile, outdir, rscpath):
|
||||
parms += '-s "' + serialnums + '" '
|
||||
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
||||
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
dpath = os.path.join(rscpath,filename)
|
||||
parms += '-k "' + dpath + '" '
|
||||
parms += '"' + infile +'" "' + outdir + '"'
|
||||
p2 = runit(cmd, parms)
|
||||
p2 = runit(apphome, cmd, parms)
|
||||
return p2
|
||||
|
||||
def processPDF(infile, outfile, rscpath):
|
||||
cmd = '"' + os.path.join('lib','decryptpdf.py') + '" '
|
||||
parms = '"' + infile + '" "' + outfile + '" "' + rscpath + '"'
|
||||
p2 = runit(cmd, parms)
|
||||
def processPDF(apphome, infile, outdir, rscpath):
|
||||
cmd = os.path.join('lib','decryptpdf.py')
|
||||
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||
p2 = runit(apphome, cmd, parms)
|
||||
return p2
|
||||
|
||||
def processEPUB(infile, outfile, rscpath):
|
||||
def processEPUB(apphome, infile, outdir, rscpath):
|
||||
# invoke routine to check both Adept and Barnes and Noble
|
||||
cmd = '"' + os.path.join('lib','decryptepub.py') + '" '
|
||||
parms = '"' + infile + '" "' + outfile + '" "' + rscpath + '"'
|
||||
p2 = runit(cmd, parms)
|
||||
cmd = os.path.join('lib','decryptepub.py')
|
||||
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||
p2 = runit(apphome, cmd, parms)
|
||||
return p2
|
||||
|
||||
def processPDB(infile, outdir, rscpath):
|
||||
cmd = '"' + os.path.join('lib','decryptpdb.py') + '" '
|
||||
def processPDB(apphome, infile, outdir, rscpath):
|
||||
cmd = os.path.join('lib','decryptpdb.py')
|
||||
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||
p2 = runit(cmd, parms)
|
||||
p2 = runit(apphome, cmd, parms)
|
||||
return p2
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
apphome = os.path.dirname(sys.argv[0])
|
||||
apphome = os.path.abspath(apphome)
|
||||
|
||||
# windows may pass a spurious quoted null string as argv[1] from bat file
|
||||
# simply work around this until we can figure out a better way to handle things
|
||||
if len(argv) == 2:
|
||||
@@ -571,7 +577,7 @@ def main(argv=sys.argv):
|
||||
filenames.append(infile)
|
||||
|
||||
# start up gui app
|
||||
app = MainApp(dnd, filenames)
|
||||
app = MainApp(apphome, dnd, filenames)
|
||||
app.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def main(argv=sys.argv):
|
||||
if len(args) != 3:
|
||||
return -1
|
||||
infile = args[0]
|
||||
outfile = args[1]
|
||||
outdir = args[1]
|
||||
rscpath = args[2]
|
||||
errlog = ''
|
||||
|
||||
@@ -37,6 +37,9 @@ def main(argv=sys.argv):
|
||||
print "Error while trying to fix epub"
|
||||
return rv
|
||||
|
||||
# determine a good name for the output file
|
||||
outfile = os.path.join(outdir, name + '_nodrm.epub')
|
||||
|
||||
rv = 1
|
||||
# first try with the Adobe adept epub
|
||||
# try with any keyfiles (*.der) in the rscpath
|
||||
|
||||
@@ -21,10 +21,15 @@ def main(argv=sys.argv):
|
||||
if len(args) != 3:
|
||||
return -1
|
||||
infile = args[0]
|
||||
outfile = args[1]
|
||||
outdir = args[1]
|
||||
rscpath = args[2]
|
||||
errlog = ''
|
||||
rv = 1
|
||||
|
||||
# determine a good name for the output file
|
||||
name, ext = os.path.splitext(os.path.basename(infile))
|
||||
outfile = os.path.join(outdir, name + '_nodrm.pdf')
|
||||
|
||||
# try with any keyfiles (*.der) in the rscpath
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||
|
||||
@@ -58,8 +58,9 @@
|
||||
# 0.17 - added support for pycrypto's DES as well
|
||||
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||
# 0.19 - Modify the interface to allow use of import
|
||||
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||
|
||||
__version__='0.19'
|
||||
__version__='0.20'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -71,32 +72,50 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
# first try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
if Des == None:
|
||||
# they try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
else:
|
||||
# first try with openssl
|
||||
import openssl_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
if Des == None:
|
||||
# then try with pycrypto
|
||||
import pycrypto_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
|
||||
# if that did not work then use pure python implementation
|
||||
# of DES and try to speed it up with Psycho
|
||||
if Des == None:
|
||||
import python_des
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import python_des
|
||||
else:
|
||||
import python_des
|
||||
Des = python_des.Des
|
||||
# Import Psyco if available
|
||||
try:
|
||||
@@ -480,5 +499,6 @@ def main(argv=None):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
||||
ys = []
|
||||
gdefs = []
|
||||
|
||||
# get path defintions, positions, dimensions for ecah glyph
|
||||
# get path defintions, positions, dimensions for each glyph
|
||||
# that makes up the image, and find min x and min y to reposition origin
|
||||
minx = -1
|
||||
miny = -1
|
||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
||||
lastGlyph = firstglyphList[last]
|
||||
else :
|
||||
lastGlyph = len(gidList)
|
||||
|
||||
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||
# by reverting to text based paragraph
|
||||
if firstGlyph >= lastGlyph:
|
||||
# revert to standard text based paragraph
|
||||
for wordnum in xrange(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||
glyphList.append(glyphnum)
|
||||
# include any extratokens if they exist
|
||||
|
||||
@@ -21,10 +21,21 @@ from struct import unpack
|
||||
|
||||
|
||||
# local support routines
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.k4mobidedrm import convert2xml
|
||||
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
|
||||
# Get a 7 bit encoded number from a file
|
||||
@@ -192,6 +203,8 @@ class GParser(object):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
def getGlyphDim(self, gly):
|
||||
if self.gdpi[gly] == 0:
|
||||
return 0, 0
|
||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||
return maxh, maxw
|
||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
print 'Processing Meta Data and creating OPF'
|
||||
meta_array = getMetaArray(metaFile)
|
||||
|
||||
# replace special chars in title and authors like & < >
|
||||
title = meta_array.get('Title','No Title Provided')
|
||||
title = title.replace('&','&')
|
||||
title = title.replace('<','<')
|
||||
title = title.replace('>','>')
|
||||
meta_array['Title'] = title
|
||||
authors = meta_array.get('Authors','No Authors Provided')
|
||||
authors = authors.replace('&','&')
|
||||
authors = authors.replace('<','<')
|
||||
authors = authors.replace('>','>')
|
||||
meta_array['Authors'] = authors
|
||||
|
||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||
metastr = ''
|
||||
for key in meta_array:
|
||||
@@ -399,8 +424,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||
htmlstr += '</head>\n<body>\n'
|
||||
|
||||
@@ -416,8 +443,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
svgindex += '</head>\n'
|
||||
svgindex += '<body>\n'
|
||||
|
||||
@@ -471,9 +500,12 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||
# adding metadata
|
||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
if 'GUID' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
if 'ASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
if 'oASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||
opfstr += ' <dc:language>en</dc:language>\n'
|
||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += ' </metadata>\n'
|
||||
opfstr += '<manifest>\n'
|
||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||
# adding image files to manifest
|
||||
filenames = os.listdir(imgDir)
|
||||
filenames = sorted(filenames)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.7
|
||||
# ineptpdf.pyw, version 7.9
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
@@ -33,6 +33,7 @@ from __future__ import with_statement
|
||||
# 7.6 - backported AES and other fixes from version 8.4.48
|
||||
# 7.7 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 7.8 - Modify interface to allow use of import
|
||||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
@@ -156,6 +157,7 @@ def _load_crypto_libcrypto():
|
||||
return out.raw
|
||||
|
||||
class AES(object):
|
||||
MODE_CBC = 0
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
self = AES()
|
||||
@@ -1531,16 +1533,30 @@ class PDFDocument(object):
|
||||
bookkey = bookkey[index:]
|
||||
ebx_V = int_value(param.get('V', 4))
|
||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||
# added because of the booktype / decryption book session key error
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
elif ebx_V < 4 or ebx_type < 6:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
# added because of improper booktype / decryption book session key errors
|
||||
if length > 0:
|
||||
if len(bookkey) == length:
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
elif len(bookkey) == length + 1:
|
||||
V = ord(bookkey[0])
|
||||
bookkey = bookkey[1:]
|
||||
else:
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||
else:
|
||||
V = 2
|
||||
if length and len(bookkey) != length:
|
||||
raise ADEPTError('error decrypting book session key')
|
||||
# proper length unknown try with whatever you have
|
||||
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
V = 2
|
||||
self.decrypt_key = bookkey
|
||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||
self.decipher = self.decrypt_rc4
|
||||
|
||||
@@ -16,20 +16,8 @@ from __future__ import with_statement
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
||||
#
|
||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
||||
# for the plugin version to function properly.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||
|
||||
|
||||
__version__ = '2.1'
|
||||
__version__ = '3.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -43,11 +31,7 @@ class Unbuffered:
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -57,24 +41,21 @@ if 'calibre' in sys.modules:
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert spaces to underscores
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
@@ -89,13 +70,10 @@ def cleanup_name(name):
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace(' ','_')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
@@ -111,8 +89,7 @@ def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
tempdir = tempfile.mkdtemp()
|
||||
mb = topazextract.TopazBook(infile, tempdir)
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
@@ -126,60 +103,39 @@ def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
if not mobi:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 1
|
||||
pass
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi')
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
return 0
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
print " Creating SVG HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
mb.getXMLZip(zipname)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -229,9 +185,11 @@ def main(argv=sys.argv):
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
@@ -239,129 +197,3 @@ if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
if not __name__ == "__main__" and inCalibre:
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 2, 1) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
import kgenpids
|
||||
import zlib
|
||||
import zipfile
|
||||
import topazextract
|
||||
import mobidedrm
|
||||
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
||||
pass
|
||||
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
tempdir = PersistentTemporaryDirectory()
|
||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
of = self.temporary_file(bookname + '.zip')
|
||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip, tempdir, 'img')
|
||||
myzip.close()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -66,9 +68,8 @@ def _load_crypto_libcrypto():
|
||||
raise DrmException('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd):
|
||||
salt = '16743'
|
||||
saltlen = 5
|
||||
def keyivgen(self, passwd, salt):
|
||||
saltlen = len(salt)
|
||||
passlen = len(passwd)
|
||||
iter = 0x3e8
|
||||
keylen = 80
|
||||
@@ -91,12 +92,78 @@ LibCrypto = _load_crypto()
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
# generate table of prime number less than or equal to int n
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
|
||||
@@ -137,30 +204,12 @@ def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
import hashlib
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
def CryptUnprotectData(encryptedData):
|
||||
def CryptUnprotectData(encryptedData, salt):
|
||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
crp = LibCrypto()
|
||||
key_iv = crp.keyivgen(passwdData)
|
||||
key_iv = crp.keyivgen(passwdData, salt)
|
||||
key = key_iv[0:32]
|
||||
iv = key_iv[32:48]
|
||||
crp.set_decrypt_key(key,iv)
|
||||
@@ -168,27 +217,141 @@ def CryptUnprotectData(encryptedData):
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate and open the .kindle-info file
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('.kindle-info')
|
||||
if pp >= 0:
|
||||
kinfopath = resline
|
||||
break
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: .kindle-info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
else:
|
||||
if not os.path.isfile(kinfoFile):
|
||||
raise DrmException('Error: kindle-info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
# Locate the .kindle-info files
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
# first search for current .kindle-info files
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
found = False
|
||||
for resline in reslst:
|
||||
if os.path.isfile(resline):
|
||||
kInfoFiles.append(resline)
|
||||
found = True
|
||||
# For Future Reference
|
||||
#
|
||||
# # add any .kinf files
|
||||
# cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"'
|
||||
# cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
# p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
# out1, out2 = p1.communicate()
|
||||
# reslst = out1.split('\n')
|
||||
# for resline in reslst:
|
||||
# if os.path.isfile(resline):
|
||||
# kInfoFiles.append(resline)
|
||||
# found = True
|
||||
if not found:
|
||||
print('No kindle-info files have been found.')
|
||||
return kInfoFiles
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('[') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
salt = '16743'
|
||||
cleartext = CryptUnprotectData(encryptedValue, salt)
|
||||
DB[keyname] = decode(cleartext,charMap1)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# For Future Reference taken from K4PC 1.5.0 .kinf
|
||||
#
|
||||
# # else newer 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('/')
|
||||
#
|
||||
# # loop through the item records until all are processed
|
||||
# while len(items) > 0:
|
||||
#
|
||||
# # get the first item record
|
||||
# item = items.pop(0)
|
||||
#
|
||||
# # the first 32 chars of the first record of a group
|
||||
# # is the MD5 hash of the key name encoded by charMap5
|
||||
# keyhash = item[0:32]
|
||||
#
|
||||
# # the raw keyhash string is also used to create entropy for the actual
|
||||
# # CryptProtectData Blob that represents that keys contents
|
||||
# entropy = SHA1(keyhash)
|
||||
#
|
||||
# # the remainder of the first record when decoded with charMap5
|
||||
# # has the ':' split char followed by the string representation
|
||||
# # of the number of records that follow
|
||||
# # and make up the contents
|
||||
# srcnt = decode(item[34:],charMap5)
|
||||
# rcnt = int(srcnt)
|
||||
#
|
||||
# # read and store in rcnt records of data
|
||||
# # that make up the contents value
|
||||
# edlst = []
|
||||
# for i in xrange(rcnt):
|
||||
# item = items.pop(0)
|
||||
# edlst.append(item)
|
||||
#
|
||||
# keyname = "unknown"
|
||||
# for name in names:
|
||||
# if encodeHash(name,charMap5) == keyhash:
|
||||
# keyname = name
|
||||
# break
|
||||
# if keyname == "unknown":
|
||||
# keyname = keyhash
|
||||
#
|
||||
# # the charMap5 encoded contents data has had a length
|
||||
# # of chars (always odd) cut off of the front and moved
|
||||
# # to the end to prevent decoding using charMap5 from
|
||||
# # working properly, and thereby preventing the ensuing
|
||||
# # CryptUnprotectData call from succeeding.
|
||||
#
|
||||
# # The offset into the charMap5 encoded contents seems to be:
|
||||
# # len(contents) - largest prime number 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)
|
||||
# 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)
|
||||
# cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys, os
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
@@ -10,25 +12,86 @@ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
|
||||
import _winreg as winreg
|
||||
|
||||
import traceback
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
import traceback
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
|
||||
# simple primes table (<= n) calculator
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
# Various character maps used to decrypt kindle info values.
|
||||
# Probably supposed to act as obfuscation
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# interface with Windows OS Routines
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
@@ -59,52 +122,175 @@ def GetVolumeSerialNumber():
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
def getLastError():
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetLastError.argtypes = None
|
||||
GetLastError.restype = c_uint
|
||||
def getLastError():
|
||||
return GetLastError()
|
||||
return getLastError
|
||||
getLastError = getLastError()
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
buffer = create_unicode_buffer(2)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
errcd = getLastError()
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
def CryptUnprotectData(indata, entropy, flags):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
None, None, flags, byref(outdata)):
|
||||
raise DrmException("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
#
|
||||
# Locate and open the Kindle.info file.
|
||||
#
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
|
||||
# Locate all of the kindle-info style files and return as list
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
|
||||
# first look for older kindle-info files
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No kindle.info files have not been found.')
|
||||
else:
|
||||
if not os.path.isfile(kInfoFile):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
kInfoFiles.append(kinfopath)
|
||||
|
||||
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No .kinf files have not been found.')
|
||||
else:
|
||||
kInfoFiles.append(kinfopath)
|
||||
return kInfoFiles
|
||||
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('{') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# else newer 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('/')
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number 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)
|
||||
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)
|
||||
cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
|
||||
|
||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
global charMap3
|
||||
global charMap4
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
@@ -66,66 +78,7 @@ def decode(data,map):
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
def parseKindleInfo(kInfoFile):
|
||||
DB = {}
|
||||
infoReader = openKindleInfo(kInfoFile)
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
if sys.platform.startswith('win'):
|
||||
items = data.split('{')
|
||||
else :
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
if sys.platform.startswith('win'):
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
else:
|
||||
cleartext = CryptUnprotectData(encryptedValue)
|
||||
return decode(cleartext, charMap1)
|
||||
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForKey(key):
|
||||
global charMap2
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
# Find if the original string for a hashed/encoded string is known.
|
||||
# If so return the original string othwise return an empty string.
|
||||
def findNameForHash(hash):
|
||||
global charMap2
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return result
|
||||
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
@@ -224,13 +177,11 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
|
||||
if rec209 != None and token != None:
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
@@ -240,15 +191,15 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
return pidlst
|
||||
|
||||
|
||||
# Parse the EXTH header records and parse the Kindleinfo
|
||||
# file to calculate the book pid.
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
global kindleDatabase
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
print(message)
|
||||
kindleDatabase = None
|
||||
@@ -256,10 +207,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
||||
|
||||
@@ -275,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
|
||||
# Compute book PID
|
||||
if rec209 == None or token == None:
|
||||
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
|
||||
return pidlst
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
# Compute book PIDs
|
||||
|
||||
# book pid
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
@@ -305,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
pidlst = getK4Pids(pidlst, md1, md2)
|
||||
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||
for infoFile in kInfoFiles:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
for serialnum in serials:
|
||||
|
||||
@@ -46,8 +46,14 @@
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They aren't for DOC compressed
|
||||
# files, but they are for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
|
||||
__version__ = '0.26'
|
||||
__version__ = '0.30'
|
||||
|
||||
import sys
|
||||
|
||||
@@ -158,6 +164,7 @@ class MobiBook:
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
@@ -175,6 +182,7 @@ class MobiBook:
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
@@ -190,7 +198,7 @@ class MobiBook:
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
if (self.mobi_version < 7) and (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
@@ -207,19 +215,16 @@ class MobiBook:
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
@@ -239,18 +244,18 @@ class MobiBook:
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
rec209 = ''
|
||||
token = ''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in xrange(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
@@ -298,13 +303,17 @@ class MobiBook:
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
@@ -350,33 +359,35 @@ class MobiBook:
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
self.mobi_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
print "done"
|
||||
return self.data_file
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
|
||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
@@ -18,10 +23,32 @@ from struct import unpack
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
import kgenpids
|
||||
import genbook
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
import kgenpids
|
||||
import genbook
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tdir, localfilePath)
|
||||
|
||||
#
|
||||
# Utility routines
|
||||
#
|
||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
||||
|
||||
|
||||
class TopazBook:
|
||||
def __init__(self, filename, outdir):
|
||||
def __init__(self, filename):
|
||||
self.fo = file(filename, 'rb')
|
||||
self.outdir = outdir
|
||||
self.outdir = tempfile.mkdtemp()
|
||||
self.bookPayloadOffset = 0
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
@@ -157,18 +184,22 @@ class TopazBook:
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
for i in range (0,nbRecords) :
|
||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
||||
self.bookMetadata[record[0]] = record[1]
|
||||
keyval = bookReadString(self.fo)
|
||||
content = bookReadString(self.fo)
|
||||
# print keyval
|
||||
# print content
|
||||
self.bookMetadata[keyval] = content
|
||||
return self.bookMetadata
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
keysRecord = None
|
||||
keysRecordRecord = None
|
||||
if 'keys' in self.bookMetadata:
|
||||
keysRecord = self.bookMetadata['keys']
|
||||
if keysRecord in self.bookMetadata:
|
||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
||||
keysRecord = self.bookMetadata.get('keys','')
|
||||
keysRecordRecord = ''
|
||||
if keysRecord != '':
|
||||
keylst = keysRecord.split(',')
|
||||
for keyval in keylst:
|
||||
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||
return keysRecord, keysRecordRecord
|
||||
|
||||
def getBookTitle(self):
|
||||
@@ -313,21 +344,33 @@ class TopazBook:
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.close()
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
@@ -379,58 +422,46 @@ def main(argv=sys.argv):
|
||||
return 1
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
tb = TopazBook(infile, tempdir)
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
tb.processBook(pidlst)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
tb.cleanup()
|
||||
return 1
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
tb.cleanup
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class fixZip:
|
||||
def __init__(self, zinput, zoutput):
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
@@ -81,30 +92,15 @@ class fixZip:
|
||||
# get the zipinfo for each member of the input archive
|
||||
# and copy member over to output archive
|
||||
# if problems exist with local vs central filename, fix them
|
||||
# also fix bad epub compression
|
||||
|
||||
# write mimetype file first, if present, and with no compression
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename == "mimetype":
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
|
||||
nzinfo.date_time = zinfo.date_time
|
||||
nzinfo.compress_type = zipfile.ZIP_STORED
|
||||
nzinfo.flag_bits = 0
|
||||
nzinfo.internal_attr = 0
|
||||
nzinfo.extra = ""
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
break
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != "mimetype":
|
||||
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ReadMe_DeDRM_WinApp_v1.2
|
||||
ReadMe_DeDRM_WinApp_vX.X
|
||||
-----------------------
|
||||
|
||||
DeDRM_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto theDeDRM_Drop_Target to have the DRM removed. It repackages the"tools" python software in one easy to use program.
|
||||
|
||||
@@ -207,11 +207,11 @@ class MainDialog(Tkinter.Frame):
|
||||
|
||||
tpz = False
|
||||
# Identify any Topaz Files
|
||||
with open(mobipath, 'rb') as f:
|
||||
raw = f.read(3)
|
||||
if raw.startswith('TPZ'):
|
||||
tpz = True
|
||||
f.close()
|
||||
f = file(mobipath, 'rb')
|
||||
raw = f.read(3)
|
||||
if raw.startswith('TPZ'):
|
||||
tpz = True
|
||||
f.close()
|
||||
if not outpath:
|
||||
self.status['text'] = 'No output directory specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
||||
ys = []
|
||||
gdefs = []
|
||||
|
||||
# get path defintions, positions, dimensions for ecah glyph
|
||||
# get path defintions, positions, dimensions for each glyph
|
||||
# that makes up the image, and find min x and min y to reposition origin
|
||||
minx = -1
|
||||
miny = -1
|
||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
||||
lastGlyph = firstglyphList[last]
|
||||
else :
|
||||
lastGlyph = len(gidList)
|
||||
|
||||
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||
# by reverting to text based paragraph
|
||||
if firstGlyph >= lastGlyph:
|
||||
# revert to standard text based paragraph
|
||||
for wordnum in xrange(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||
glyphList.append(glyphnum)
|
||||
# include any extratokens if they exist
|
||||
@@ -21,10 +21,21 @@ from struct import unpack
|
||||
|
||||
|
||||
# local support routines
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.k4mobidedrm import convert2xml
|
||||
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
|
||||
# Get a 7 bit encoded number from a file
|
||||
@@ -192,6 +203,8 @@ class GParser(object):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
def getGlyphDim(self, gly):
|
||||
if self.gdpi[gly] == 0:
|
||||
return 0, 0
|
||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||
return maxh, maxw
|
||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
print 'Processing Meta Data and creating OPF'
|
||||
meta_array = getMetaArray(metaFile)
|
||||
|
||||
# replace special chars in title and authors like & < >
|
||||
title = meta_array.get('Title','No Title Provided')
|
||||
title = title.replace('&','&')
|
||||
title = title.replace('<','<')
|
||||
title = title.replace('>','>')
|
||||
meta_array['Title'] = title
|
||||
authors = meta_array.get('Authors','No Authors Provided')
|
||||
authors = authors.replace('&','&')
|
||||
authors = authors.replace('<','<')
|
||||
authors = authors.replace('>','>')
|
||||
meta_array['Authors'] = authors
|
||||
|
||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||
metastr = ''
|
||||
for key in meta_array:
|
||||
@@ -399,8 +424,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||
htmlstr += '</head>\n<body>\n'
|
||||
|
||||
@@ -416,8 +443,10 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
if 'ASIN' in meta_array:
|
||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||
if 'GUID' in meta_array:
|
||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||
svgindex += '</head>\n'
|
||||
svgindex += '<body>\n'
|
||||
|
||||
@@ -471,9 +500,12 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||
# adding metadata
|
||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
if 'GUID' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||
if 'ASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||
if 'oASIN' in meta_array:
|
||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||
opfstr += ' <dc:language>en</dc:language>\n'
|
||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
||||
opfstr += ' </metadata>\n'
|
||||
opfstr += '<manifest>\n'
|
||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||
# adding image files to manifest
|
||||
filenames = os.listdir(imgDir)
|
||||
filenames = sorted(filenames)
|
||||
199
KindleBooks/lib/k4mobidedrm.py
Normal file
199
KindleBooks/lib/k4mobidedrm.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
|
||||
__version__ = '3.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import re
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||
from calibre_plugins.k4mobidedrm import topazextract
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert spaces to underscores
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace(' ','_')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = bookname
|
||||
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||
outfilename = outfilename + "_" + filenametitle
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
return 0
|
||||
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
mb.getXMLZip(zipname)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
357
KindleBooks/lib/k4mutils.py
Normal file
357
KindleBooks/lib/k4mutils.py
Normal file
@@ -0,0 +1,357 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# interface to needed routines in openssl's libcrypto
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
||||
|
||||
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
|
||||
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||
|
||||
class LibCrypto(object):
|
||||
def __init__(self):
|
||||
self._blocksize = 0
|
||||
self._keyctx = None
|
||||
self.iv = 0
|
||||
|
||||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self.iv = iv
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd, salt):
|
||||
saltlen = len(salt)
|
||||
passlen = len(passwd)
|
||||
iter = 0x3e8
|
||||
keylen = 80
|
||||
out = create_string_buffer(keylen)
|
||||
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
||||
return out.raw
|
||||
return LibCrypto
|
||||
|
||||
def _load_crypto():
|
||||
LibCrypto = None
|
||||
try:
|
||||
LibCrypto = _load_crypto_libcrypto()
|
||||
except (ImportError, DrmException):
|
||||
pass
|
||||
return LibCrypto
|
||||
|
||||
LibCrypto = _load_crypto()
|
||||
|
||||
#
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
# For Future Reference from .kinf approach of K4PC
|
||||
# generate table of prime number less than or equal to int n
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
return sernum
|
||||
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||
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)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
if (bsdname == 'disk0') and (sernum != None):
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
sernum = '9999999999'
|
||||
return sernum
|
||||
|
||||
# uses unix env to get username instead of using sysctlbyname
|
||||
def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
def CryptUnprotectData(encryptedData, salt):
|
||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
crp = LibCrypto()
|
||||
key_iv = crp.keyivgen(passwdData, salt)
|
||||
key = key_iv[0:32]
|
||||
iv = key_iv[32:48]
|
||||
crp.set_decrypt_key(key,iv)
|
||||
cleartext = crp.decrypt(encryptedData)
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate the .kindle-info files
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
# first search for current .kindle-info files
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
found = False
|
||||
for resline in reslst:
|
||||
if os.path.isfile(resline):
|
||||
kInfoFiles.append(resline)
|
||||
found = True
|
||||
# For Future Reference
|
||||
#
|
||||
# # add any .kinf files
|
||||
# cmdline = 'find "' + home + '/Library/Application Support" -name "rainier*.kinf"'
|
||||
# cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
# p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
# out1, out2 = p1.communicate()
|
||||
# reslst = out1.split('\n')
|
||||
# for resline in reslst:
|
||||
# if os.path.isfile(resline):
|
||||
# kInfoFiles.append(resline)
|
||||
# found = True
|
||||
if not found:
|
||||
print('No kindle-info files have been found.')
|
||||
return kInfoFiles
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('[') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
salt = '16743'
|
||||
cleartext = CryptUnprotectData(encryptedValue, salt)
|
||||
DB[keyname] = decode(cleartext,charMap1)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# For Future Reference taken from K4PC 1.5.0 .kinf
|
||||
#
|
||||
# # else newer 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('/')
|
||||
#
|
||||
# # loop through the item records until all are processed
|
||||
# while len(items) > 0:
|
||||
#
|
||||
# # get the first item record
|
||||
# item = items.pop(0)
|
||||
#
|
||||
# # the first 32 chars of the first record of a group
|
||||
# # is the MD5 hash of the key name encoded by charMap5
|
||||
# keyhash = item[0:32]
|
||||
#
|
||||
# # the raw keyhash string is also used to create entropy for the actual
|
||||
# # CryptProtectData Blob that represents that keys contents
|
||||
# entropy = SHA1(keyhash)
|
||||
#
|
||||
# # the remainder of the first record when decoded with charMap5
|
||||
# # has the ':' split char followed by the string representation
|
||||
# # of the number of records that follow
|
||||
# # and make up the contents
|
||||
# srcnt = decode(item[34:],charMap5)
|
||||
# rcnt = int(srcnt)
|
||||
#
|
||||
# # read and store in rcnt records of data
|
||||
# # that make up the contents value
|
||||
# edlst = []
|
||||
# for i in xrange(rcnt):
|
||||
# item = items.pop(0)
|
||||
# edlst.append(item)
|
||||
#
|
||||
# keyname = "unknown"
|
||||
# for name in names:
|
||||
# if encodeHash(name,charMap5) == keyhash:
|
||||
# keyname = name
|
||||
# break
|
||||
# if keyname == "unknown":
|
||||
# keyname = keyhash
|
||||
#
|
||||
# # the charMap5 encoded contents data has had a length
|
||||
# # of chars (always odd) cut off of the front and moved
|
||||
# # to the end to prevent decoding using charMap5 from
|
||||
# # working properly, and thereby preventing the ensuing
|
||||
# # CryptUnprotectData call from succeeding.
|
||||
#
|
||||
# # The offset into the charMap5 encoded contents seems to be:
|
||||
# # len(contents) - largest prime number 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)
|
||||
# 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)
|
||||
# cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
296
KindleBooks/lib/k4pcutils.py
Normal file
296
KindleBooks/lib/k4pcutils.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys, os
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
|
||||
import _winreg as winreg
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
import traceback
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
|
||||
# simple primes table (<= n) calculator
|
||||
def primes(n):
|
||||
if n==2: return [2]
|
||||
elif n<2: return []
|
||||
s=range(3,n+1,2)
|
||||
mroot = n ** 0.5
|
||||
half=(n+1)/2-1
|
||||
i=0
|
||||
m=3
|
||||
while m <= mroot:
|
||||
if s[i]:
|
||||
j=(m*m-3)/2
|
||||
s[j]=0
|
||||
while j<half:
|
||||
s[j]=0
|
||||
j+=m
|
||||
i=i+1
|
||||
m=2*i+3
|
||||
return [2]+[x for x in s if x]
|
||||
|
||||
|
||||
# Various character maps used to decrypt kindle info values.
|
||||
# Probably supposed to act as obfuscation
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# interface with Windows OS Routines
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
DataBlob_p = POINTER(DataBlob)
|
||||
|
||||
|
||||
def GetSystemDirectory():
|
||||
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||
GetSystemDirectoryW.restype = c_uint
|
||||
def GetSystemDirectory():
|
||||
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||
GetSystemDirectoryW(buffer, len(buffer))
|
||||
return buffer.value
|
||||
return GetSystemDirectory
|
||||
GetSystemDirectory = GetSystemDirectory()
|
||||
|
||||
def GetVolumeSerialNumber():
|
||||
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||
POINTER(c_uint), POINTER(c_uint),
|
||||
POINTER(c_uint), c_wchar_p, c_uint]
|
||||
GetVolumeInformationW.restype = c_uint
|
||||
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
|
||||
vsn = c_uint(0)
|
||||
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
||||
return str(vsn.value)
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
def getLastError():
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetLastError.argtypes = None
|
||||
GetLastError.restype = c_uint
|
||||
def getLastError():
|
||||
return GetLastError()
|
||||
return getLastError
|
||||
getLastError = getLastError()
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(2)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
errcd = getLastError()
|
||||
if errcd == 234:
|
||||
# bad wine implementation up through wine 1.3.21
|
||||
return "AlternateUserName"
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy, flags):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, flags, byref(outdata)):
|
||||
raise DrmException("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
|
||||
# Locate all of the kindle-info style files and return as list
|
||||
def getKindleInfoFiles(kInfoFiles):
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
|
||||
# first look for older kindle-info files
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No kindle.info files have not been found.')
|
||||
else:
|
||||
kInfoFiles.append(kinfopath)
|
||||
|
||||
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||
if not os.path.isfile(kinfopath):
|
||||
print('No .kinf files have not been found.')
|
||||
else:
|
||||
kInfoFiles.append(kinfopath)
|
||||
return kInfoFiles
|
||||
|
||||
|
||||
# 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"]
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
hdr = infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
|
||||
if data.find('{') != -1 :
|
||||
|
||||
# older style kindle-info file
|
||||
items = data.split('{')
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||
cnt = cnt + 1
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
# else newer 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('/')
|
||||
|
||||
# loop through the item records until all are processed
|
||||
while len(items) > 0:
|
||||
|
||||
# get the first item record
|
||||
item = items.pop(0)
|
||||
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
# has the ':' split char followed by the string representation
|
||||
# of the number of records that follow
|
||||
# and make up the contents
|
||||
srcnt = decode(item[34:],charMap5)
|
||||
rcnt = int(srcnt)
|
||||
|
||||
# read and store in rcnt records of data
|
||||
# that make up the contents value
|
||||
edlst = []
|
||||
for i in xrange(rcnt):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
# of chars (always odd) cut off of the front and moved
|
||||
# to the end to prevent decoding using charMap5 from
|
||||
# working properly, and thereby preventing the ensuing
|
||||
# CryptUnprotectData call from succeeding.
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number 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)
|
||||
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)
|
||||
cnt = cnt + 1
|
||||
|
||||
if cnt == 0:
|
||||
DB = None
|
||||
return DB
|
||||
|
||||
|
||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
global charMap3
|
||||
global charMap4
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetVolumeSerialNumber
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
@@ -66,66 +78,7 @@ def decode(data,map):
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
def parseKindleInfo(kInfoFile):
|
||||
DB = {}
|
||||
infoReader = openKindleInfo(kInfoFile)
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
if sys.platform.startswith('win'):
|
||||
items = data.split('{')
|
||||
else :
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
global charMap1
|
||||
global charMap2
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
if sys.platform.startswith('win'):
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
else:
|
||||
cleartext = CryptUnprotectData(encryptedValue)
|
||||
return decode(cleartext, charMap1)
|
||||
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext).
|
||||
# Return the decoded and decrypted record
|
||||
def getKindleInfoValueForKey(key):
|
||||
global charMap2
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
# Find if the original string for a hashed/encoded string is known.
|
||||
# If so return the original string othwise return an empty string.
|
||||
def findNameForHash(hash):
|
||||
global charMap2
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return result
|
||||
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
@@ -224,13 +177,11 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
|
||||
if rec209 != None and token != None:
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
@@ -240,15 +191,15 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
return pidlst
|
||||
|
||||
|
||||
# Parse the EXTH header records and parse the Kindleinfo
|
||||
# file to calculate the book pid.
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
global kindleDatabase
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
print(message)
|
||||
kindleDatabase = None
|
||||
@@ -256,10 +207,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
||||
|
||||
@@ -275,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
|
||||
# Compute book PID
|
||||
if rec209 == None or token == None:
|
||||
print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?"
|
||||
return pidlst
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
# Compute book PIDs
|
||||
|
||||
# book pid
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
@@ -305,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
||||
|
||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
pidlst = getK4Pids(pidlst, md1, md2)
|
||||
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||
for infoFile in kInfoFiles:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
for serialnum in serials:
|
||||
@@ -46,8 +46,14 @@
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They aren't for DOC compressed
|
||||
# files, but they are for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
|
||||
__version__ = '0.26'
|
||||
__version__ = '0.30'
|
||||
|
||||
import sys
|
||||
|
||||
@@ -158,6 +164,7 @@ class MobiBook:
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
@@ -175,6 +182,7 @@ class MobiBook:
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
@@ -190,7 +198,7 @@ class MobiBook:
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
if (self.mobi_version < 7) and (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
@@ -207,19 +215,16 @@ class MobiBook:
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
@@ -239,18 +244,18 @@ class MobiBook:
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
rec209 = ''
|
||||
token = ''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in xrange(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
@@ -298,13 +303,17 @@ class MobiBook:
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
@@ -350,33 +359,35 @@ class MobiBook:
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
self.mobi_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
print "done"
|
||||
return self.data_file
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
@@ -18,10 +23,32 @@ from struct import unpack
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
import kgenpids
|
||||
import genbook
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
import kgenpids
|
||||
import genbook
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tdir, localfilePath)
|
||||
|
||||
#
|
||||
# Utility routines
|
||||
#
|
||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
||||
|
||||
|
||||
class TopazBook:
|
||||
def __init__(self, filename, outdir):
|
||||
def __init__(self, filename):
|
||||
self.fo = file(filename, 'rb')
|
||||
self.outdir = outdir
|
||||
self.outdir = tempfile.mkdtemp()
|
||||
self.bookPayloadOffset = 0
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
@@ -157,18 +184,22 @@ class TopazBook:
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
for i in range (0,nbRecords) :
|
||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
||||
self.bookMetadata[record[0]] = record[1]
|
||||
keyval = bookReadString(self.fo)
|
||||
content = bookReadString(self.fo)
|
||||
# print keyval
|
||||
# print content
|
||||
self.bookMetadata[keyval] = content
|
||||
return self.bookMetadata
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
keysRecord = None
|
||||
keysRecordRecord = None
|
||||
if 'keys' in self.bookMetadata:
|
||||
keysRecord = self.bookMetadata['keys']
|
||||
if keysRecord in self.bookMetadata:
|
||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
||||
keysRecord = self.bookMetadata.get('keys','')
|
||||
keysRecordRecord = ''
|
||||
if keysRecord != '':
|
||||
keylst = keysRecord.split(',')
|
||||
for keyval in keylst:
|
||||
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||
return keysRecord, keysRecordRecord
|
||||
|
||||
def getBookTitle(self):
|
||||
@@ -313,21 +344,33 @@ class TopazBook:
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.close()
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
@@ -379,58 +422,46 @@ def main(argv=sys.argv):
|
||||
return 1
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
tb = TopazBook(infile, tempdir)
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
tb.processBook(pidlst)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
tb.cleanup()
|
||||
return 1
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
tb.cleanup
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# This is a simple tool to identify all Amazon Topaz ebooks in a specific directory.
|
||||
# There always seems to be confusion since Topaz books downloaded to K4PC/Mac can have
|
||||
# almost any extension (.azw, .azw1, .prc, tpz). While the .azw1 and .tpz extensions
|
||||
# are fairly easy to indentify, the others are not (without opening the files in an editor).
|
||||
|
||||
# To run the tool with the GUI frontend, just double-click on the 'FindTopazFiles.pyw' file
|
||||
# and select the folder where all of the ebooks in question are located. Then click 'Search'.
|
||||
# The program will list the file names of the ebooks that are indentified as being Topaz.
|
||||
# You can then isolate those books and use the Topaz tools to decrypt and convert them.
|
||||
|
||||
# You can also run the script from a command line... supplying the folder to search
|
||||
# as a parameter: python FindTopazEbooks.pyw "C:\My Folder" (change appropriately for
|
||||
# your particular O.S.)
|
||||
|
||||
# ** NOTE: This program does NOT decrypt or modify Topaz files in any way. It simply identifies them.
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release.
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
|
||||
class ScrolledText(Tkinter.Text):
|
||||
def __init__(self, master=None, **kw):
|
||||
self.frame = Tkinter.Frame(master)
|
||||
self.vbar = Tkinter.Scrollbar(self.frame)
|
||||
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
|
||||
kw.update({'yscrollcommand': self.vbar.set})
|
||||
Tkinter.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
|
||||
self.vbar['command'] = self.yview
|
||||
# Copy geometry methods of self.frame without overriding Text
|
||||
# methods = hack!
|
||||
text_meths = vars(Tkinter.Text).keys()
|
||||
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
|
||||
methods = set(methods).difference(text_meths)
|
||||
for m in methods:
|
||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
||||
setattr(self, m, getattr(self.frame, m))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.frame)
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv, obj=None):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 2:
|
||||
print "usage: %s DIRECTORY" % (progname,)
|
||||
return 1
|
||||
|
||||
if obj == None:
|
||||
print "\nTopaz search results:\n"
|
||||
else:
|
||||
obj.stext.insert(Tkconstants.END,"Topaz search results:\n\n")
|
||||
|
||||
inpath = argv[1]
|
||||
files = os.listdir(inpath)
|
||||
filefilter = re.compile("(\.azw$)|(\.azw1$)|(\.prc$)|(\.tpz$)", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
topazcount = 0
|
||||
totalcount = 0
|
||||
for filename in files:
|
||||
with open(os.path.join(inpath, filename), 'rb') as f:
|
||||
try:
|
||||
if f.read().startswith('TPZ'):
|
||||
f.close()
|
||||
basename, extension = os.path.splitext(filename)
|
||||
if obj == None:
|
||||
print " %s is a Topaz formatted ebook." % filename
|
||||
"""
|
||||
if extension == '.azw' or extension == '.prc':
|
||||
print " renaming to %s" % (basename + '.tpz')
|
||||
shutil.move(os.path.join(inpath, filename),
|
||||
os.path.join(inpath, basename + '.tpz'))
|
||||
"""
|
||||
else:
|
||||
msg1 = " %s is a Topaz formatted ebook.\n" % filename
|
||||
obj.stext.insert(Tkconstants.END,msg1)
|
||||
"""
|
||||
if extension == '.azw' or extension == '.prc':
|
||||
msg2 = " renaming to %s\n" % (basename + '.tpz')
|
||||
obj.stext.insert(Tkconstants.END,msg2)
|
||||
shutil.move(os.path.join(inpath, filename),
|
||||
os.path.join(inpath, basename + '.tpz'))
|
||||
"""
|
||||
topazcount += 1
|
||||
except:
|
||||
if obj == None:
|
||||
print " Error reading %s." % filename
|
||||
else:
|
||||
msg = " Error reading or %s.\n" % filename
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
pass
|
||||
totalcount += 1
|
||||
if topazcount == 0:
|
||||
if obj == None:
|
||||
print "\nNo Topaz books found in %s." % inpath
|
||||
else:
|
||||
msg = "\nNo Topaz books found in %s.\n\n" % inpath
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
else:
|
||||
if obj == None:
|
||||
print "\n%i Topaz books found in %s\n%i total books checked.\n" % (topazcount, inpath, totalcount)
|
||||
else:
|
||||
msg = "\n%i Topaz books found in %s\n%i total books checked.\n\n" %(topazcount, inpath, totalcount)
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
else:
|
||||
if obj == None:
|
||||
print "No typical Topaz file extensions found in %s.\n" % inpath
|
||||
else:
|
||||
msg = "No typical Topaz file extensions found in %s.\n\n" % inpath
|
||||
obj.stext.insert(Tkconstants.END,msg)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Search a directory for Topaz eBooks\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Directory to Search').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
msg1 = 'Topaz search results \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE,
|
||||
height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky)
|
||||
#self.stext.insert(Tkconstants.END,msg1)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
|
||||
|
||||
self.botton = Tkinter.Button(
|
||||
buttons, text="Search", width=10, command=self.search)
|
||||
self.botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
self.button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_inpath(self):
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
inpath = tkFileDialog.askdirectory(
|
||||
parent=None, title='Directory to search',
|
||||
initialdir=cwd, initialfile=None)
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
|
||||
def search(self):
|
||||
inpath = self.inpath.get()
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified directory does not exist'
|
||||
return
|
||||
argv = [sys.argv[0], inpath]
|
||||
self.status['text'] = 'Searching...'
|
||||
self.botton.configure(state='disabled')
|
||||
cli_main(argv, self)
|
||||
self.status['text'] = 'Search a directory for Topaz files'
|
||||
self.botton.configure(state='normal')
|
||||
|
||||
return
|
||||
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
root.title('Topaz eBook Finder')
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
@@ -1,900 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
Comprehensive Mazama Book DRM with Topaz Cryptography V2.2
|
||||
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
|
||||
M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
|
||||
B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
|
||||
y2/pHuYme7U1TsgSjwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
import getopt
|
||||
import zlib
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
import _winreg as winreg
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
import hashlib
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
global kindleDatabase
|
||||
global bookFile
|
||||
global bookPayloadOffset
|
||||
global bookHeaderRecords
|
||||
global bookMetadata
|
||||
global bookKey
|
||||
global command
|
||||
|
||||
#
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
#
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
#
|
||||
# Exceptions for all the problems that might happen during the script
|
||||
#
|
||||
|
||||
class CMBDTCError(Exception):
|
||||
pass
|
||||
|
||||
class CMBDTCFatal(Exception):
|
||||
pass
|
||||
|
||||
#
|
||||
# Stolen stuff
|
||||
#
|
||||
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
DataBlob_p = POINTER(DataBlob)
|
||||
|
||||
def GetSystemDirectory():
|
||||
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||
GetSystemDirectoryW.restype = c_uint
|
||||
def GetSystemDirectory():
|
||||
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||
GetSystemDirectoryW(buffer, len(buffer))
|
||||
return buffer.value
|
||||
return GetSystemDirectory
|
||||
GetSystemDirectory = GetSystemDirectory()
|
||||
|
||||
|
||||
def GetVolumeSerialNumber():
|
||||
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||
POINTER(c_uint), POINTER(c_uint),
|
||||
POINTER(c_uint), c_wchar_p, c_uint]
|
||||
GetVolumeInformationW.restype = c_uint
|
||||
def GetVolumeSerialNumber(path):
|
||||
vsn = c_uint(0)
|
||||
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
||||
return vsn.value
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
raise CMBDTCFatal("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
#
|
||||
# Returns the MD5 digest of "message"
|
||||
#
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
#
|
||||
# Returns the MD5 digest of "message"
|
||||
#
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
#
|
||||
# Open the book file at path
|
||||
#
|
||||
|
||||
def openBook(path):
|
||||
try:
|
||||
return open(path,'rb')
|
||||
except:
|
||||
raise CMBDTCFatal("Could not open book file: " + path)
|
||||
#
|
||||
# Encode the bytes in data with the characters in map
|
||||
#
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
#
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
#
|
||||
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
#
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
#
|
||||
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data),2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
value = (((high * 0x40) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
#
|
||||
# Locate and open the Kindle.info file (Hopefully in the way it is done in the Kindle application)
|
||||
#
|
||||
|
||||
def openKindleInfo():
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
|
||||
|
||||
#
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
#
|
||||
|
||||
def parseKindleInfo():
|
||||
DB = {}
|
||||
infoReader = openKindleInfo()
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
items = data.split('{')
|
||||
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
#
|
||||
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
|
||||
#
|
||||
|
||||
def findNameForHash(hash):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return name
|
||||
|
||||
#
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
#
|
||||
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------\n")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
#
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
|
||||
#
|
||||
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
|
||||
#
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
|
||||
#
|
||||
|
||||
def getKindleInfoValueForKey(key):
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
#
|
||||
# Get a 7 bit encoded number from the book file
|
||||
#
|
||||
|
||||
def bookReadEncodedNumber():
|
||||
flag = False
|
||||
data = ord(bookFile.read(1))
|
||||
|
||||
if data == 0xFF:
|
||||
flag = True
|
||||
data = ord(bookFile.read(1))
|
||||
|
||||
if data >= 0x80:
|
||||
datax = (data & 0x7F)
|
||||
while data >= 0x80 :
|
||||
data = ord(bookFile.read(1))
|
||||
datax = (datax <<7) + (data & 0x7F)
|
||||
data = datax
|
||||
|
||||
if flag:
|
||||
data = -data
|
||||
return data
|
||||
|
||||
#
|
||||
# Encode a number in 7 bit format
|
||||
#
|
||||
|
||||
def encodeNumber(number):
|
||||
result = ""
|
||||
negative = False
|
||||
flag = 0
|
||||
|
||||
if number < 0 :
|
||||
number = -number + 1
|
||||
negative = True
|
||||
|
||||
while True:
|
||||
byte = number & 0x7F
|
||||
number = number >> 7
|
||||
byte += flag
|
||||
result += chr(byte)
|
||||
flag = 0x80
|
||||
if number == 0 :
|
||||
if (byte == 0xFF and negative == False) :
|
||||
result += chr(0x80)
|
||||
break
|
||||
|
||||
if negative:
|
||||
result += chr(0xFF)
|
||||
|
||||
return result[::-1]
|
||||
|
||||
#
|
||||
# Get a length prefixed string from the file
|
||||
#
|
||||
|
||||
def bookReadString():
|
||||
stringLength = bookReadEncodedNumber()
|
||||
return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# Returns a length prefixed string
|
||||
#
|
||||
|
||||
def lengthPrefixString(data):
|
||||
return encodeNumber(len(data))+data
|
||||
|
||||
|
||||
#
|
||||
# Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...]
|
||||
#
|
||||
|
||||
def bookReadHeaderRecordData():
|
||||
nbValues = bookReadEncodedNumber()
|
||||
values = []
|
||||
for i in range (0,nbValues):
|
||||
values.append([bookReadEncodedNumber(),bookReadEncodedNumber(),bookReadEncodedNumber()])
|
||||
return values
|
||||
|
||||
#
|
||||
# Read and parse one header record at the current book file position and return the associated data [[offset,compressedLength,decompressedLength],...]
|
||||
#
|
||||
|
||||
def parseTopazHeaderRecord():
|
||||
if ord(bookFile.read(1)) != 0x63:
|
||||
raise CMBDTCFatal("Parse Error : Invalid Header")
|
||||
|
||||
tag = bookReadString()
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
|
||||
#
|
||||
# Parse the header of a Topaz file, get all the header records and the offset for the payload
|
||||
#
|
||||
|
||||
def parseTopazHeader():
|
||||
global bookHeaderRecords
|
||||
global bookPayloadOffset
|
||||
magic = unpack("4s",bookFile.read(4))[0]
|
||||
|
||||
if magic != 'TPZ0':
|
||||
raise CMBDTCFatal("Parse Error : Invalid Header, not a Topaz file")
|
||||
|
||||
nbRecords = bookReadEncodedNumber()
|
||||
bookHeaderRecords = {}
|
||||
|
||||
for i in range (0,nbRecords):
|
||||
result = parseTopazHeaderRecord()
|
||||
bookHeaderRecords[result[0]] = result[1]
|
||||
|
||||
if ord(bookFile.read(1)) != 0x64 :
|
||||
raise CMBDTCFatal("Parse Error : Invalid Header")
|
||||
|
||||
bookPayloadOffset = bookFile.tell()
|
||||
|
||||
#
|
||||
# Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed
|
||||
#
|
||||
|
||||
def getBookPayloadRecord(name, index):
|
||||
encrypted = False
|
||||
|
||||
try:
|
||||
recordOffset = bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise CMBDTCFatal("Parse Error : Invalid Record, record not found")
|
||||
|
||||
bookFile.seek(bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString()
|
||||
if tag != name :
|
||||
raise CMBDTCFatal("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber()
|
||||
|
||||
if recordIndex < 0 :
|
||||
encrypted = True
|
||||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise CMBDTCFatal("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if bookHeaderRecords[name][index][2] != 0 :
|
||||
record = bookFile.read(bookHeaderRecords[name][index][2])
|
||||
else:
|
||||
record = bookFile.read(bookHeaderRecords[name][index][1])
|
||||
|
||||
if encrypted:
|
||||
ctx = topazCryptoInit(bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
|
||||
return record
|
||||
|
||||
#
|
||||
# Extract, decrypt and decompress a book record indicated by name and index and print it or save it in "filename"
|
||||
#
|
||||
|
||||
def extractBookPayloadRecord(name, index, filename):
|
||||
compressed = False
|
||||
|
||||
try:
|
||||
compressed = bookHeaderRecords[name][index][2] != 0
|
||||
record = getBookPayloadRecord(name,index)
|
||||
except:
|
||||
print("Could not find record")
|
||||
|
||||
if compressed:
|
||||
try:
|
||||
record = zlib.decompress(record)
|
||||
except:
|
||||
raise CMBDTCFatal("Could not decompress record")
|
||||
|
||||
if filename != "":
|
||||
try:
|
||||
file = open(filename,"wb")
|
||||
file.write(record)
|
||||
file.close()
|
||||
except:
|
||||
raise CMBDTCFatal("Could not write to destination file")
|
||||
else:
|
||||
print(record)
|
||||
|
||||
#
|
||||
# return next record [key,value] from the book metadata from the current book position
|
||||
#
|
||||
|
||||
def readMetadataRecord():
|
||||
return [bookReadString(),bookReadString()]
|
||||
|
||||
#
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
#
|
||||
|
||||
def parseMetadata():
|
||||
global bookHeaderRecords
|
||||
global bookPayloadAddress
|
||||
global bookMetadata
|
||||
bookMetadata = {}
|
||||
bookFile.seek(bookPayloadOffset + bookHeaderRecords["metadata"][0][0])
|
||||
tag = bookReadString()
|
||||
if tag != "metadata" :
|
||||
raise CMBDTCFatal("Parse Error : Record Names Don't Match")
|
||||
|
||||
flags = ord(bookFile.read(1))
|
||||
nbRecords = ord(bookFile.read(1))
|
||||
|
||||
for i in range (0,nbRecords) :
|
||||
record =readMetadataRecord()
|
||||
bookMetadata[record[0]] = record[1]
|
||||
|
||||
#
|
||||
# Returns two bit at offset from a bit field
|
||||
#
|
||||
|
||||
def getTwoBitsFromBitField(bitField,offset):
|
||||
byteNumber = offset // 4
|
||||
bitPosition = 6 - 2*(offset % 4)
|
||||
|
||||
return ord(bitField[byteNumber]) >> bitPosition & 3
|
||||
|
||||
#
|
||||
# Returns the six bits at offset from a bit field
|
||||
#
|
||||
|
||||
def getSixBitsFromBitField(bitField,offset):
|
||||
offset *= 3
|
||||
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
|
||||
return value
|
||||
|
||||
#
|
||||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
#
|
||||
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
|
||||
#
|
||||
# Context initialisation for the Topaz Crypto
|
||||
#
|
||||
|
||||
def topazCryptoInit(key):
|
||||
ctx1 = 0x0CAFFE19E
|
||||
|
||||
for keyChar in key:
|
||||
keyByte = ord(keyChar)
|
||||
ctx2 = ctx1
|
||||
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||
return [ctx1,ctx2]
|
||||
|
||||
#
|
||||
# decrypt data with the context prepared by topazCryptoInit()
|
||||
#
|
||||
|
||||
def topazCryptoDecrypt(data, ctx):
|
||||
ctx1 = ctx[0]
|
||||
ctx2 = ctx[1]
|
||||
|
||||
plainText = ""
|
||||
|
||||
for dataChar in data:
|
||||
dataByte = ord(dataChar)
|
||||
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||
ctx2 = ctx1
|
||||
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||
plainText += chr(m)
|
||||
|
||||
return plainText
|
||||
|
||||
#
|
||||
# Decrypt a payload record with the PID
|
||||
#
|
||||
|
||||
def decryptRecord(data,PID):
|
||||
ctx = topazCryptoInit(PID)
|
||||
return topazCryptoDecrypt(data, ctx)
|
||||
|
||||
#
|
||||
# Try to decrypt a dkey record (contains the book PID)
|
||||
#
|
||||
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack("3sB8sB8s3s",record)
|
||||
|
||||
if fields[0] != "PID" or fields[5] != "pid" :
|
||||
raise CMBDTCError("Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise CMBDTCError("Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise CMBDTCError("Record didn't contain PID")
|
||||
|
||||
return fields[4]
|
||||
|
||||
#
|
||||
# Decrypt all the book's dkey records (contain the book PID)
|
||||
#
|
||||
|
||||
def decryptDkeyRecords(data,PID):
|
||||
nbKeyRecords = ord(data[0])
|
||||
records = []
|
||||
data = data[1:]
|
||||
for i in range (0,nbKeyRecords):
|
||||
length = ord(data[0])
|
||||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except CMBDTCError:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
|
||||
return records
|
||||
|
||||
#
|
||||
# Encryption table used to generate the device PID
|
||||
#
|
||||
|
||||
def generatePidEncryptionTable() :
|
||||
table = []
|
||||
for counter1 in range (0,0x100):
|
||||
value = counter1
|
||||
for counter2 in range (0,8):
|
||||
if (value & 1 == 0) :
|
||||
value = value >> 1
|
||||
else :
|
||||
value = value >> 1
|
||||
value = value ^ 0xEDB88320
|
||||
table.append(value)
|
||||
return table
|
||||
|
||||
#
|
||||
# Seed value used to generate the device PID
|
||||
#
|
||||
|
||||
def generatePidSeed(table,dsn) :
|
||||
value = 0
|
||||
for counter in range (0,4) :
|
||||
index = (ord(dsn[counter]) ^ value) &0xFF
|
||||
value = (value >> 8) ^ table[index]
|
||||
return value
|
||||
|
||||
#
|
||||
# Generate the device PID
|
||||
#
|
||||
|
||||
def generateDevicePID(table,dsn,nbRoll):
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
|
||||
for counter in range (0,nbRoll):
|
||||
pid[index] = pid[index] ^ ord(dsn[counter])
|
||||
index = (index+1) %8
|
||||
|
||||
for counter in range (0,8):
|
||||
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
||||
pidAscii += charMap4[index]
|
||||
return pidAscii
|
||||
|
||||
#
|
||||
# Create decrypted book payload
|
||||
#
|
||||
|
||||
def createDecryptedPayload(payload):
|
||||
|
||||
# store data to be able to create the header later
|
||||
headerData= []
|
||||
currentOffset = 0
|
||||
|
||||
# Add social DRM to decrypted files
|
||||
|
||||
try:
|
||||
data = getKindleInfoValueForKey("kindle.name.info")+":"+ getKindleInfoValueForKey("login")
|
||||
if payload!= None:
|
||||
payload.write(lengthPrefixString("sdrm"))
|
||||
payload.write(encodeNumber(0))
|
||||
payload.write(data)
|
||||
else:
|
||||
currentOffset += len(lengthPrefixString("sdrm"))
|
||||
currentOffset += len(encodeNumber(0))
|
||||
currentOffset += len(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
for headerRecord in bookHeaderRecords:
|
||||
name = headerRecord
|
||||
newRecord = []
|
||||
|
||||
if name != "dkey" :
|
||||
|
||||
for index in range (0,len(bookHeaderRecords[name])) :
|
||||
offset = currentOffset
|
||||
|
||||
if payload != None:
|
||||
# write tag
|
||||
payload.write(lengthPrefixString(name))
|
||||
# write data
|
||||
payload.write(encodeNumber(index))
|
||||
payload.write(getBookPayloadRecord(name, index))
|
||||
|
||||
else :
|
||||
currentOffset += len(lengthPrefixString(name))
|
||||
currentOffset += len(encodeNumber(index))
|
||||
currentOffset += len(getBookPayloadRecord(name, index))
|
||||
newRecord.append([offset,bookHeaderRecords[name][index][1],bookHeaderRecords[name][index][2]])
|
||||
|
||||
headerData.append([name,newRecord])
|
||||
|
||||
|
||||
|
||||
return headerData
|
||||
|
||||
#
|
||||
# Create decrypted book
|
||||
#
|
||||
|
||||
def createDecryptedBook(outputFile):
|
||||
outputFile = open(outputFile,"wb")
|
||||
# Write the payload in a temporary file
|
||||
headerData = createDecryptedPayload(None)
|
||||
outputFile.write("TPZ0")
|
||||
outputFile.write(encodeNumber(len(headerData)))
|
||||
|
||||
for header in headerData :
|
||||
outputFile.write(chr(0x63))
|
||||
outputFile.write(lengthPrefixString(header[0]))
|
||||
outputFile.write(encodeNumber(len(header[1])))
|
||||
for numbers in header[1] :
|
||||
outputFile.write(encodeNumber(numbers[0]))
|
||||
outputFile.write(encodeNumber(numbers[1]))
|
||||
outputFile.write(encodeNumber(numbers[2]))
|
||||
|
||||
outputFile.write(chr(0x64))
|
||||
createDecryptedPayload(outputFile)
|
||||
outputFile.close()
|
||||
|
||||
#
|
||||
# Set the command to execute by the programm according to cmdLine parameters
|
||||
#
|
||||
|
||||
def setCommand(name) :
|
||||
global command
|
||||
if command != "" :
|
||||
raise CMBDTCFatal("Invalid command line parameters")
|
||||
else :
|
||||
command = name
|
||||
|
||||
#
|
||||
# Program usage
|
||||
#
|
||||
|
||||
def usage():
|
||||
print("\nUsage:")
|
||||
print("\nCMBDTC.py [options] bookFileName\n")
|
||||
print("-p Adds a PID to the list of PIDs that are tried to decrypt the book key (can be used several times)")
|
||||
print("-d Saves a decrypted copy of the book")
|
||||
print("-r Prints or writes to disk a record indicated in the form name:index (e.g \"img:0\")")
|
||||
print("-o Output file name to write records and decrypted books")
|
||||
print("-v Verbose (can be used several times)")
|
||||
print("-i Prints kindle.info database")
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
|
||||
def main(argv=sys.argv):
|
||||
global kindleDatabase
|
||||
global bookMetadata
|
||||
global bookKey
|
||||
global bookFile
|
||||
global command
|
||||
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
verbose = 0
|
||||
recordName = ""
|
||||
recordIndex = 0
|
||||
outputFile = ""
|
||||
PIDs = []
|
||||
kindleDatabase = None
|
||||
command = ""
|
||||
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "vdir:o:p:")
|
||||
except getopt.GetoptError, err:
|
||||
# print help information and exit:
|
||||
print str(err) # will print something like "option -a not recognized"
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
if len(opts) == 0 and len(args) == 0 :
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-v":
|
||||
verbose+=1
|
||||
if o == "-i":
|
||||
setCommand("printInfo")
|
||||
if o =="-o":
|
||||
if a == None :
|
||||
raise CMBDTCFatal("Invalid parameter for -o")
|
||||
outputFile = a
|
||||
if o =="-r":
|
||||
setCommand("printRecord")
|
||||
try:
|
||||
recordName,recordIndex = a.split(':')
|
||||
except:
|
||||
raise CMBDTCFatal("Invalid parameter for -r")
|
||||
if o =="-p":
|
||||
PIDs.append(a)
|
||||
if o =="-d":
|
||||
setCommand("doit")
|
||||
|
||||
if command == "" :
|
||||
raise CMBDTCFatal("No action supplied on command line")
|
||||
|
||||
#
|
||||
# Read the encrypted database
|
||||
#
|
||||
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo()
|
||||
except Exception, message:
|
||||
if verbose>0:
|
||||
print(message)
|
||||
|
||||
if kindleDatabase != None :
|
||||
if command == "printInfo" :
|
||||
printKindleInfo()
|
||||
|
||||
#
|
||||
# Compute the DSN
|
||||
#
|
||||
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1)
|
||||
|
||||
# Get the current user name
|
||||
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||
|
||||
# concat, hash and encode
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
||||
|
||||
if verbose >1:
|
||||
print("DSN: " + DSN)
|
||||
|
||||
#
|
||||
# Compute the device PID
|
||||
#
|
||||
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
PIDs.append(devicePID)
|
||||
|
||||
if verbose > 0:
|
||||
print("Device PID: " + devicePID)
|
||||
|
||||
#
|
||||
# Open book and parse metadata
|
||||
#
|
||||
|
||||
if len(args) == 1:
|
||||
|
||||
bookFile = openBook(args[0])
|
||||
parseTopazHeader()
|
||||
parseMetadata()
|
||||
|
||||
#
|
||||
# Compute book PID
|
||||
#
|
||||
|
||||
# Get the account token
|
||||
|
||||
if kindleDatabase != None:
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
|
||||
if verbose >1:
|
||||
print("Account Token: " + kindleAccountToken)
|
||||
|
||||
keysRecord = bookMetadata["keys"]
|
||||
keysRecordRecord = bookMetadata[keysRecord]
|
||||
|
||||
pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord)
|
||||
|
||||
bookPID = encodePID(pidHash)
|
||||
PIDs.append(bookPID)
|
||||
|
||||
if verbose > 0:
|
||||
print ("Book PID: " + bookPID )
|
||||
|
||||
#
|
||||
# Decrypt book key
|
||||
#
|
||||
|
||||
dkey = getBookPayloadRecord('dkey', 0)
|
||||
|
||||
bookKeys = []
|
||||
for PID in PIDs :
|
||||
bookKeys+=decryptDkeyRecords(dkey,PID)
|
||||
|
||||
if len(bookKeys) == 0 :
|
||||
if verbose > 0 :
|
||||
print ("Book key could not be found. Maybe this book is not registered with this device.")
|
||||
else :
|
||||
bookKey = bookKeys[0]
|
||||
if verbose > 0:
|
||||
print("Book key: " + bookKey.encode('hex'))
|
||||
|
||||
|
||||
|
||||
if command == "printRecord" :
|
||||
extractBookPayloadRecord(recordName,int(recordIndex),outputFile)
|
||||
if outputFile != "" and verbose>0 :
|
||||
print("Wrote record to file: "+outputFile)
|
||||
elif command == "doit" :
|
||||
if outputFile!="" :
|
||||
createDecryptedBook(outputFile)
|
||||
if verbose >0 :
|
||||
print ("Decrypted book saved. Don't pirate!")
|
||||
elif verbose > 0:
|
||||
print("Output file name was not supplied.")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
# For use with Topaz Scripts Version 2.6
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
|
||||
import os, getopt
|
||||
|
||||
# local routines
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import decode_meta
|
||||
|
||||
|
||||
def usage():
|
||||
print 'Usage: '
|
||||
print ' '
|
||||
print ' genxml.py dict0000.dat unencryptedBookDir'
|
||||
print ' '
|
||||
|
||||
|
||||
|
||||
def main(argv):
|
||||
bookDir = ''
|
||||
|
||||
if len(argv) == 0:
|
||||
argv = sys.argv
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "h:")
|
||||
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage()
|
||||
sys.exit(1)
|
||||
|
||||
if len(opts) == 0 and len(args) == 0 :
|
||||
usage()
|
||||
sys.exit(1)
|
||||
|
||||
for o, a in opts:
|
||||
if o =="-h":
|
||||
usage()
|
||||
sys.exit(0)
|
||||
|
||||
bookDir = args[0]
|
||||
|
||||
if not os.path.exists(bookDir) :
|
||||
print "Can not find directory with unencrypted book"
|
||||
sys.exit(1)
|
||||
|
||||
dictFile = os.path.join(bookDir,'dict0000.dat')
|
||||
if not os.path.exists(dictFile) :
|
||||
print "Can not find dict0000.dat file"
|
||||
sys.exit(1)
|
||||
|
||||
pageDir = os.path.join(bookDir,'page')
|
||||
if not os.path.exists(pageDir) :
|
||||
print "Can not find page directory in unencrypted book"
|
||||
sys.exit(1)
|
||||
|
||||
glyphsDir = os.path.join(bookDir,'glyphs')
|
||||
if not os.path.exists(glyphsDir) :
|
||||
print "Can not find glyphs directory in unencrypted book"
|
||||
sys.exit(1)
|
||||
|
||||
otherFile = os.path.join(bookDir,'other0000.dat')
|
||||
if not os.path.exists(otherFile) :
|
||||
print "Can not find other0000.dat in unencrypted book"
|
||||
sys.exit(1)
|
||||
|
||||
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
||||
if not os.path.exists(metaFile) :
|
||||
print "Can not find metadata0000.dat in unencrypted book"
|
||||
sys.exit(1)
|
||||
|
||||
xmlDir = os.path.join(bookDir,'xml')
|
||||
if not os.path.exists(xmlDir):
|
||||
os.makedirs(xmlDir)
|
||||
|
||||
|
||||
print 'Processing ... '
|
||||
|
||||
print ' ', 'metadata0000.dat'
|
||||
fname = os.path.join(bookDir,'metadata0000.dat')
|
||||
xname = os.path.join(xmlDir, 'metadata.txt')
|
||||
metastr = decode_meta.getMetaData(fname)
|
||||
file(xname, 'wb').write(metastr)
|
||||
|
||||
print ' ', 'other0000.dat'
|
||||
fname = os.path.join(bookDir,'other0000.dat')
|
||||
xname = os.path.join(xmlDir, 'stylesheet.xml')
|
||||
pargv=[]
|
||||
pargv.append('convert2xml.py')
|
||||
pargv.append(dictFile)
|
||||
pargv.append(fname)
|
||||
xmlstr = convert2xml.main(pargv)
|
||||
file(xname, 'wb').write(xmlstr)
|
||||
|
||||
filenames = os.listdir(pageDir)
|
||||
filenames = sorted(filenames)
|
||||
|
||||
for filename in filenames:
|
||||
print ' ', filename
|
||||
fname = os.path.join(pageDir,filename)
|
||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||
pargv=[]
|
||||
pargv.append('convert2xml.py')
|
||||
pargv.append(dictFile)
|
||||
pargv.append(fname)
|
||||
xmlstr = convert2xml.main(pargv)
|
||||
file(xname, 'wb').write(xmlstr)
|
||||
|
||||
filenames = os.listdir(glyphsDir)
|
||||
filenames = sorted(filenames)
|
||||
|
||||
for filename in filenames:
|
||||
print ' ', filename
|
||||
fname = os.path.join(glyphsDir,filename)
|
||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||
pargv=[]
|
||||
pargv.append('convert2xml.py')
|
||||
pargv.append(dictFile)
|
||||
pargv.append(fname)
|
||||
xmlstr = convert2xml.main(pargv)
|
||||
file(xname, 'wb').write(xmlstr)
|
||||
|
||||
|
||||
print 'Processing Complete'
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(''))
|
||||
@@ -1,367 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and eBook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
|
||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
||||
#
|
||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
||||
# for the plugin version to function properly.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||
|
||||
|
||||
__version__ = '2.1'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
|
||||
# cleanup bytestring filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
tempdir = tempfile.mkdtemp()
|
||||
mb = topazextract.TopazBook(infile, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = bookname
|
||||
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||
outfilename = outfilename + "_" + filenametitle
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
return 1
|
||||
except Exception, e:
|
||||
if not mobi:
|
||||
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 1
|
||||
pass
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi')
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir, True)
|
||||
return 0
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
if not __name__ == "__main__" and inCalibre:
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 2, 1) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
import kgenpids
|
||||
import zlib
|
||||
import zipfile
|
||||
import topazextract
|
||||
import mobidedrm
|
||||
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
kInfoFiles.append(fpath)
|
||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
||||
except IOError:
|
||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
||||
pass
|
||||
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
tempdir = PersistentTemporaryDirectory()
|
||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
of = self.temporary_file(bookname + '.zip')
|
||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip, tempdir, 'img')
|
||||
myzip.close()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
||||
@@ -1,194 +0,0 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# interface to needed routines in openssl's libcrypto
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
||||
|
||||
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
|
||||
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||
|
||||
class LibCrypto(object):
|
||||
def __init__(self):
|
||||
self._blocksize = 0
|
||||
self._keyctx = None
|
||||
self.iv = 0
|
||||
|
||||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self.iv = iv
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd):
|
||||
salt = '16743'
|
||||
saltlen = 5
|
||||
passlen = len(passwd)
|
||||
iter = 0x3e8
|
||||
keylen = 80
|
||||
out = create_string_buffer(keylen)
|
||||
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
||||
return out.raw
|
||||
return LibCrypto
|
||||
|
||||
def _load_crypto():
|
||||
LibCrypto = None
|
||||
try:
|
||||
LibCrypto = _load_crypto_libcrypto()
|
||||
except (ImportError, DrmException):
|
||||
pass
|
||||
return LibCrypto
|
||||
|
||||
LibCrypto = _load_crypto()
|
||||
|
||||
#
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
return sernum
|
||||
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||
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)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
if (bsdname == 'disk0') and (sernum != None):
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
sernum = '9999999999'
|
||||
return sernum
|
||||
|
||||
# uses unix env to get username instead of using sysctlbyname
|
||||
def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
import hashlib
|
||||
|
||||
def SHA256(message):
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||
def CryptUnprotectData(encryptedData):
|
||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
||||
passwdData = encode(SHA256(sp),charMap1)
|
||||
crp = LibCrypto()
|
||||
key_iv = crp.keyivgen(passwdData)
|
||||
key = key_iv[0:32]
|
||||
iv = key_iv[32:48]
|
||||
crp.set_decrypt_key(key,iv)
|
||||
cleartext = crp.decrypt(encryptedData)
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate and open the .kindle-info file
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('.kindle-info')
|
||||
if pp >= 0:
|
||||
kinfopath = resline
|
||||
break
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: .kindle-info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
else:
|
||||
if not os.path.isfile(kinfoFile):
|
||||
raise DrmException('Error: kindle-info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
@@ -1,110 +0,0 @@
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys, os
|
||||
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
|
||||
import _winreg as winreg
|
||||
|
||||
import traceback
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
DataBlob_p = POINTER(DataBlob)
|
||||
|
||||
|
||||
def GetSystemDirectory():
|
||||
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||
GetSystemDirectoryW.restype = c_uint
|
||||
def GetSystemDirectory():
|
||||
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||
GetSystemDirectoryW(buffer, len(buffer))
|
||||
return buffer.value
|
||||
return GetSystemDirectory
|
||||
GetSystemDirectory = GetSystemDirectory()
|
||||
|
||||
def GetVolumeSerialNumber():
|
||||
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||
POINTER(c_uint), POINTER(c_uint),
|
||||
POINTER(c_uint), c_wchar_p, c_uint]
|
||||
GetVolumeInformationW.restype = c_uint
|
||||
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
|
||||
vsn = c_uint(0)
|
||||
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
||||
return str(vsn.value)
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
raise DrmException("Failed to Unprotect Data")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
#
|
||||
# Locate and open the Kindle.info file.
|
||||
#
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||
if not os.path.isfile(kinfopath):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kinfopath,'r')
|
||||
else:
|
||||
if not os.path.isfile(kInfoFile):
|
||||
raise DrmException('Error: kindle.info file can not be found')
|
||||
return open(kInfoFile, 'r')
|
||||
@@ -1,406 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusion has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
# 0.15 - Now outputs 'heartbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
# 0.17 - added modifications to support its use as an imported python module
|
||||
# both inside calibre and also in other places (ie K4DeDRM tools)
|
||||
# 0.17a- disabled the standalone plugin feature since a plugin can not import
|
||||
# a plugin
|
||||
# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
|
||||
# Removed the disabled Calibre plug-in code
|
||||
# Permit use of 8-digit PIDs
|
||||
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
|
||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||
# 0.21 - Added support for multiple pids
|
||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||
# 0.23 - fixed problem with older files with no EXTH section
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
|
||||
__version__ = '0.26'
|
||||
|
||||
import sys
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# MobiBook Utility Routines
|
||||
#
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Check the low bit to see if there's multibyte data present.
|
||||
# if multibyte data is included in the encryped data, we'll
|
||||
# have already cleared this flag.
|
||||
if flags & 1:
|
||||
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
|
||||
|
||||
class MobiBook:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
# build up section offset and flag info
|
||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_version = -1
|
||||
self.meta_array = {}
|
||||
return
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
|
||||
# if exth region exists parse it for metadata array
|
||||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
pass
|
||||
|
||||
def getBookTitle(self):
|
||||
title = ''
|
||||
if 503 in self.meta_array:
|
||||
title = self.meta_array[503]
|
||||
else :
|
||||
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
|
||||
tend = toff + tlen
|
||||
title = self.sect[toff:tend]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
found_key = None
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if found_key != None:
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver:
|
||||
found_key = finalkey
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
print "done"
|
||||
return self.data_file
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,299 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
import binascii
|
||||
import hashlib
|
||||
|
||||
|
||||
#
|
||||
# Returns the SHA1 digest of "message"
|
||||
#
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.hexdigest()
|
||||
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Remove Encryption from Kindle for Mac Mobi eBook')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Locate your Kindle Applications').grid(row=0, sticky=Tkconstants.E)
|
||||
self.k4mpath = Tkinter.Entry(body, width=50)
|
||||
self.k4mpath.grid(row=0, column=1, sticky=sticky)
|
||||
self.appname = '/Applications/Kindle for Mac.app'
|
||||
if not os.path.exists(self.appname):
|
||||
self.appname = '/Applications/Kindle.app'
|
||||
cwd = self.appname
|
||||
cwd = cwd.encode('utf-8')
|
||||
self.k4mpath.insert(0, cwd)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_k4mpath)
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
Tkinter.Label(body, text='Directory for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
|
||||
self.outpath = Tkinter.Entry(body, width=50)
|
||||
self.outpath.grid(row=1, column=1, sticky=sticky)
|
||||
desktoppath = os.getenv('HOME') + '/Desktop/'
|
||||
desktoppath = desktoppath.encode('utf-8')
|
||||
self.outpath.insert(0, desktoppath)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=1, column=2)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Encryption successfully removed\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
msg = msg.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def mobirdr(self, infile, outfile, pidnum):
|
||||
cmdline = 'python ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
def get_k4mpath(self):
|
||||
k4mpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select Your Kindle Application',
|
||||
defaultextension='.app', filetypes=[('Kindle for Mac Application', '.app')])
|
||||
|
||||
if k4mpath:
|
||||
k4mpath = os.path.normpath(k4mpath)
|
||||
self.k4mpath.delete(0, Tkconstants.END)
|
||||
self.k4mpath.insert(0, k4mpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
outpath = tkFileDialog.askdirectory(
|
||||
parent=None, title='Directory to Put Non-DRM eBook into',
|
||||
initialdir=cwd, initialfile=None)
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# run as a gdb subprocess via pipes and collect stdout
|
||||
def gdbrdr(self, k4mappfile, gdbcmds):
|
||||
cmdline = '/usr/bin/gdb -q -silent -readnow -batch -x ' + gdbcmds + ' "' + k4mappfile + '"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p3 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
poll = p3.wait('wait')
|
||||
results = p3.read()
|
||||
pidnum = 'NOTAPID+'
|
||||
topazbook = 0
|
||||
bookpath = 'book not found'
|
||||
# parse the gdb results to get the last pid and the last azw/prc file name in the gdb listing
|
||||
reslst = results.split('\n')
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('PID is ')
|
||||
if pp == 0:
|
||||
pidnum = resline[7:]
|
||||
topazbook = 0
|
||||
if pp > 0:
|
||||
pidnum = resline[13:]
|
||||
topazbook = 1
|
||||
fp = resline.find('File is ')
|
||||
if fp >= 0:
|
||||
tp1 = resline.find('.azw')
|
||||
tp2 = resline.find('.prc')
|
||||
tp3 = resline.find('.mbp')
|
||||
if tp1 >= 0 or tp2 >= 0:
|
||||
bookpath = resline[8:]
|
||||
if tp3 >= 0 and topazbook == 1:
|
||||
bookpath = resline[8:-3]
|
||||
bookpath += 'azw'
|
||||
# put code here to get pid and file name
|
||||
return pidnum, bookpath, topazbook
|
||||
|
||||
# convert from 8 digit PID to proper 10 digit PID
|
||||
def checksumPid(self, s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
# start the process
|
||||
def convertit(self):
|
||||
# dictionary of all known Kindle for Mac Binaries
|
||||
sha1_app_digests = {
|
||||
'e197ed2171ceb44a35c24bd30263b7253331694f' : 'gdb_kindle_cmds_r1.txt',
|
||||
'4f702436171f84acc13bdf9f94fae91525aecef5' : 'gdb_kindle_cmds_r2.txt',
|
||||
'4981b7eb37ccf0b8f63f56e8024b5ab593e8a97c' : 'gdb_kindle_cmds_r3.txt',
|
||||
'82909f0545688f09343e2c8fd8521eeee37d2de6' : 'gdb_kindle_cmds_r4.txt',
|
||||
'e260e3515cd525cd085c70baa6e42e08079edbcd' : 'gdb_kindle_cmds_r4.txt',
|
||||
'no_sha1_digest_key_here_________________' : 'no_gdb_kindle_cmds.txt',
|
||||
}
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
|
||||
k4mpath = self.k4mpath.get()
|
||||
outpath = self.outpath.get()
|
||||
|
||||
# basic error checking
|
||||
if not k4mpath or not os.path.exists(k4mpath):
|
||||
self.status['text'] = 'Error: Specified Kindle for Mac Application does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Error: No output directory specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not os.path.isdir(outpath):
|
||||
self.status['text'] = 'Error specified outputdirectory does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not os.path.isfile('/usr/bin/gdb'):
|
||||
self.status['text'] = 'Error: gdb does not exist, install the XCode Develoepr Tools'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
|
||||
# now check if the K4M app binary is known and if so which gdbcmds to use
|
||||
binary_app_file = k4mpath + '/Contents/MacOS/Kindle for Mac'
|
||||
if not os.path.exists(binary_app_file):
|
||||
binary_app_file = k4mpath + '/Contents/MacOS/Kindle'
|
||||
|
||||
k4mpath = binary_app_file
|
||||
|
||||
digest = SHA1(file(binary_app_file, 'rb').read())
|
||||
|
||||
# print digest
|
||||
gdbcmds = None
|
||||
if digest in sha1_app_digests:
|
||||
gdbcmds = sha1_app_digests[digest]
|
||||
else :
|
||||
self.status['text'] = 'Error: Kindle Application does not match any known version, sha1sum is ' + digest
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
# run Kindle for Mac in gdb to get what we need
|
||||
(pidnum, bookpath, topazbook) = self.gdbrdr(k4mpath, gdbcmds)
|
||||
|
||||
if topazbook == 1:
|
||||
log = 'Warning: ' + bookpath + ' is a Topaz book\n'
|
||||
log += '\n\n'
|
||||
log += 'To convert this book please use the Topaz Tools\n'
|
||||
log += 'With the 8 digit PID: "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
pidnum = self.checksumPid(pidnum)
|
||||
|
||||
# default output file name to be input file name + '_nodrm.mobi'
|
||||
initname = os.path.splitext(os.path.basename(bookpath))[0]
|
||||
initname += '_nodrm.mobi'
|
||||
outpath += '/' + initname
|
||||
|
||||
log = 'Command = "python mobidedrm.py"\n'
|
||||
log += 'Mobi Path = "'+ bookpath + '"\n'
|
||||
log += 'Output file = "' + outpath + '"\n'
|
||||
log += 'PID = "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.mobirdr(bookpath, outpath, pidnum)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Kindle for Mac eBook Encryption Removal')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,61 +0,0 @@
|
||||
K4Munswindle
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Kindle for Mac.app Version 1.0.0 Beta 1 (27214)
|
||||
or
|
||||
Kindle.app Version 1.2.0 (30689)
|
||||
or
|
||||
Kindle.app Version 1.2.1 (30781)
|
||||
or
|
||||
Kindle.app Version 1.2.2 (30814)
|
||||
(this is now the current version)
|
||||
|
||||
- A **recent** version of the XCode Developer Tools **must** be Installed
|
||||
(see your latest Mac OS X Install Disk for the installer, and then use Apple System Updates)
|
||||
|
||||
***PLEASE REMEMBER to UNCHECK the "auto updates" in the Kindle.app Preferences!
|
||||
***otherwise it will always update and K4MUnswindle will stop working
|
||||
|
||||
|
||||
The directions for use are:
|
||||
|
||||
1. double-click on K4Munswindle.pyw
|
||||
|
||||
In the window that opens:
|
||||
|
||||
– hit the first '...' button to locate your Kindle Application
|
||||
if it is not in /Applications
|
||||
|
||||
– hit the second '...' button to select an output directory
|
||||
(defaults to your Desktop)
|
||||
|
||||
– hit the 'Start' button
|
||||
|
||||
After a short delay, your Kindle application should open up automagically
|
||||
|
||||
2. In Kindle for Mac:
|
||||
|
||||
- hit the “Home” button to go home.
|
||||
|
||||
- double-click on ONE of your books.
|
||||
This should open the book.
|
||||
|
||||
3. Once the book you want is open
|
||||
|
||||
- hit the “Home” button and then exit the Kindle for Mac application
|
||||
|
||||
4. Once you have exited the Kindle for Mac application you should see one of the following:
|
||||
|
||||
- If the book you selected was a Topaz Book:
|
||||
|
||||
A Warning message will appear in the Conversion Log indicating
|
||||
that the book you opened was Topaz, along with the 8 digit PID
|
||||
needed to convert it using Topaz_Tools
|
||||
|
||||
- If the book you selected was a Mobi book:
|
||||
|
||||
MobiDeDRM will be automagically started in the Conversion Log
|
||||
window and if successful you should find your decoded book in
|
||||
the output directory.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
set verbose 0
|
||||
break * 0x00b2a56a
|
||||
commands 1
|
||||
printf "PID is %s\n", $edx
|
||||
continue
|
||||
end
|
||||
break * 0x014ca2af
|
||||
commands 2
|
||||
printf "File is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
condition 2 $eax != 0
|
||||
run
|
||||
@@ -1,18 +0,0 @@
|
||||
set verbose 0
|
||||
break * 0x00e37be4
|
||||
commands 1
|
||||
printf "PID is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
break * 0x0142cd94
|
||||
commands 2
|
||||
printf "File is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
condition 2 $eax != 0
|
||||
break * 0x01009c88
|
||||
commands 3
|
||||
printf "TOPAZ PID is %s\n", *(long*)($esp+12)
|
||||
continue
|
||||
end
|
||||
run
|
||||
@@ -1,18 +0,0 @@
|
||||
set verbose 0
|
||||
break * 0x00d3dac4
|
||||
commands 1
|
||||
printf "PID is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
break * 0x0130a384
|
||||
commands 2
|
||||
printf "File is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
condition 2 $eax != 0
|
||||
break * 0x00f0f306
|
||||
commands 3
|
||||
printf "TOPAZ PID is %s\n", *(long*)($esp+12)
|
||||
continue
|
||||
end
|
||||
run
|
||||
@@ -1,18 +0,0 @@
|
||||
set verbose 0
|
||||
break * 0x00d8f72a
|
||||
commands 1
|
||||
printf "PID is %s\n", *(long*)($esp+4)
|
||||
continue
|
||||
end
|
||||
break * 0x0127498c
|
||||
commands 2
|
||||
printf "File is %s\n", $eax
|
||||
continue
|
||||
end
|
||||
condition 2 $eax != 0
|
||||
break * 0x00e9aec0
|
||||
commands 3
|
||||
printf "TOPAZ PID is %s\n", **(long**)($esp+8)
|
||||
continue
|
||||
end
|
||||
run
|
||||
@@ -1,406 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusion has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
# 0.15 - Now outputs 'heartbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
# 0.17 - added modifications to support its use as an imported python module
|
||||
# both inside calibre and also in other places (ie K4DeDRM tools)
|
||||
# 0.17a- disabled the standalone plugin feature since a plugin can not import
|
||||
# a plugin
|
||||
# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
|
||||
# Removed the disabled Calibre plug-in code
|
||||
# Permit use of 8-digit PIDs
|
||||
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
|
||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||
# 0.21 - Added support for multiple pids
|
||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||
# 0.23 - fixed problem with older files with no EXTH section
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
|
||||
__version__ = '0.26'
|
||||
|
||||
import sys
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# MobiBook Utility Routines
|
||||
#
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Check the low bit to see if there's multibyte data present.
|
||||
# if multibyte data is included in the encryped data, we'll
|
||||
# have already cleared this flag.
|
||||
if flags & 1:
|
||||
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
|
||||
|
||||
class MobiBook:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile):
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
# build up section offset and flag info
|
||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_version = -1
|
||||
self.meta_array = {}
|
||||
return
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
if self.mobi_version < 7:
|
||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
|
||||
# if exth region exists parse it for metadata array
|
||||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
content = "\144"
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
content = "\0"
|
||||
else:
|
||||
content = exth[pos + 8: pos + size]
|
||||
#print type, size, content
|
||||
self.meta_array[type] = content
|
||||
pos += size
|
||||
except:
|
||||
self.meta_array = {}
|
||||
pass
|
||||
|
||||
def getBookTitle(self):
|
||||
title = ''
|
||||
if 503 in self.meta_array:
|
||||
title = self.meta_array[503]
|
||||
else :
|
||||
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
|
||||
tend = toff + tlen
|
||||
title = self.sect[toff:tend]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return title
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = None
|
||||
token = None
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
found_key = None
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if found_key != None:
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver:
|
||||
found_key = finalkey
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||
if extra_size > 0:
|
||||
new_data += data[-extra_size:]
|
||||
if self.num_sections > self.records+1:
|
||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
||||
self.data_file = new_data
|
||||
print "done"
|
||||
return self.data_file
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook([pid])
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
return book.processBook(pidlist)
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
|
||||
# basic scrolled text widget
|
||||
class ScrolledText(Tkinter.Text):
|
||||
def __init__(self, master=None, **kw):
|
||||
self.frame = Tkinter.Frame(master)
|
||||
self.vbar = Tkinter.Scrollbar(self.frame)
|
||||
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
|
||||
kw.update({'yscrollcommand': self.vbar.set})
|
||||
Tkinter.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
|
||||
self.vbar['command'] = self.yview
|
||||
# Copy geometry methods of self.frame without overriding Text
|
||||
# methods = hack!
|
||||
text_meths = vars(Tkinter.Text).keys()
|
||||
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
|
||||
methods = set(methods).difference(text_meths)
|
||||
for m in methods:
|
||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
||||
setattr(self, m, getattr(self.frame, m))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.frame)
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import os, sys
|
||||
import signal
|
||||
import threading
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
# **heavily** chopped up and modfied version of asyncproc.py
|
||||
# to make it actually work on Windows as well as Mac/Linux
|
||||
# For the original see:
|
||||
# "http://www.lysator.liu.se/~bellman/download/"
|
||||
# author is "Thomas Bellman <bellman@lysator.liu.se>"
|
||||
# available under GPL version 3 or Later
|
||||
|
||||
# create an asynchronous subprocess whose output can be collected in
|
||||
# a non-blocking manner
|
||||
|
||||
# What a mess! Have to use threads just to get non-blocking io
|
||||
# in a cross-platform manner
|
||||
|
||||
# luckily all thread use is hidden within this class
|
||||
|
||||
class Process(object):
|
||||
def __init__(self, *params, **kwparams):
|
||||
if len(params) <= 3:
|
||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
||||
if len(params) <= 4:
|
||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
||||
if len(params) <= 5:
|
||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
||||
self.__pending_input = []
|
||||
self.__collected_outdata = []
|
||||
self.__collected_errdata = []
|
||||
self.__exitstatus = None
|
||||
self.__lock = threading.Lock()
|
||||
self.__inputsem = threading.Semaphore(0)
|
||||
self.__quit = False
|
||||
|
||||
self.__process = subprocess.Popen(*params, **kwparams)
|
||||
|
||||
if self.__process.stdin:
|
||||
self.__stdin_thread = threading.Thread(
|
||||
name="stdin-thread",
|
||||
target=self.__feeder, args=(self.__pending_input,
|
||||
self.__process.stdin))
|
||||
self.__stdin_thread.setDaemon(True)
|
||||
self.__stdin_thread.start()
|
||||
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
def pid(self):
|
||||
return self.__process.pid
|
||||
|
||||
def kill(self, signal):
|
||||
self.__process.send_signal(signal)
|
||||
|
||||
# check on subprocess (pass in 'nowait') to act like poll
|
||||
def wait(self, flag):
|
||||
if flag.lower() == 'nowait':
|
||||
rc = self.__process.poll()
|
||||
else:
|
||||
rc = self.__process.wait()
|
||||
if rc != None:
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread.join()
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread.join()
|
||||
return self.__process.returncode
|
||||
|
||||
def terminate(self):
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
self.__process.terminate()
|
||||
|
||||
# thread gets data from subprocess stdout
|
||||
def __reader(self, collector, source):
|
||||
while True:
|
||||
data = os.read(source.fileno(), 65536)
|
||||
self.__lock.acquire()
|
||||
collector.append(data)
|
||||
self.__lock.release()
|
||||
if data == "":
|
||||
source.close()
|
||||
break
|
||||
return
|
||||
|
||||
# thread feeds data to subprocess stdin
|
||||
def __feeder(self, pending, drain):
|
||||
while True:
|
||||
self.__inputsem.acquire()
|
||||
self.__lock.acquire()
|
||||
if not pending and self.__quit:
|
||||
drain.close()
|
||||
self.__lock.release()
|
||||
break
|
||||
data = pending.pop(0)
|
||||
self.__lock.release()
|
||||
drain.write(data)
|
||||
|
||||
# non-blocking read of data from subprocess stdout
|
||||
def read(self):
|
||||
self.__lock.acquire()
|
||||
outdata = "".join(self.__collected_outdata)
|
||||
del self.__collected_outdata[:]
|
||||
self.__lock.release()
|
||||
return outdata
|
||||
|
||||
# non-blocking read of data from subprocess stderr
|
||||
def readerr(self):
|
||||
self.__lock.acquire()
|
||||
errdata = "".join(self.__collected_errdata)
|
||||
del self.__collected_errdata[:]
|
||||
self.__lock.release()
|
||||
return errdata
|
||||
|
||||
# non-blocking write to stdin of subprocess
|
||||
def write(self, data):
|
||||
if self.__process.stdin is None:
|
||||
raise ValueError("Writing to process with stdin not a pipe")
|
||||
self.__lock.acquire()
|
||||
self.__pending_input.append(data)
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
# close stdinput of subprocess
|
||||
def closeinput(self):
|
||||
self.__lock.acquire()
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
README
|
||||
|
||||
unswindle can be used to find the book specific PID but it needs to be updated for each version of Kindle4PC that Amazon releases (and therefore is also useful for Linux users who have Wine). This program “patches” the Kindle4PC executable and therefore is very release specific.
|
||||
|
||||
Unfortunately unswindle v7 the latest, has not been updated to work with the latest version of Kindle for PC. You will need to find one of the older versions of Kindle4PC and prevent later updates in order to use this tool.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user