tools v2.1
combined kindle/mobi plugin
This commit is contained in:
287
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/K4Munswindle.pyw
Normal file
287
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/K4Munswindle.pyw
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/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 = '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')
|
||||
if tp1 >= 0 or tp2 >= 0:
|
||||
bookpath = resline[8:]
|
||||
# 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',
|
||||
'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 bianry 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'
|
||||
|
||||
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)
|
||||
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())
|
||||
@@ -0,0 +1,58 @@
|
||||
K4MUnswindle
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Kindle for Mac.app Version 1.0.0 Beta 1 (27214)
|
||||
(this is the original version)
|
||||
|
||||
or
|
||||
|
||||
Kindle.app Version 1.2.0 (30689)
|
||||
(this is the current version at Amazon)
|
||||
|
||||
|
||||
- XCode Developer Tools **must** be Installed
|
||||
(see your latest Mac OS X Install Disk for the installer)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -0,0 +1,18 @@
|
||||
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
|
||||
310
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
Normal file
310
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
Normal file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# It can run standalone to convert files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that
|
||||
# importing files with DRM 'Just Works'.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file and import that Calibre
|
||||
# using its plugin configuration GUI.
|
||||
#
|
||||
# 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 confusin 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 'hearbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
|
||||
__version__ = '0.16'
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
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)
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
# 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
|
||||
# Multibyte data, if present, is included in the encryption, so
|
||||
# we do not need to check the low bit.
|
||||
# if flags & 1:
|
||||
# num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
class DrmStripper:
|
||||
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 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, pid):
|
||||
pid = pid.ljust(16,'\0')
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
temp_key = PC1(keyvec1, pid, 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])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
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])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum:
|
||||
found_key = finalkey
|
||||
break
|
||||
return found_key
|
||||
|
||||
def __init__(self, data_file, pid):
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
raise DrmException("invalid PID checksum")
|
||||
pid = pid[0:-2]
|
||||
|
||||
self.data_file = data_file
|
||||
header = data_file[0:72]
|
||||
if header[0x3C:0x3C+8] != 'BOOKMOBI':
|
||||
raise DrmException("invalid file format")
|
||||
self.num_sections, = struct.unpack('>H', data_file[76:78])
|
||||
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
sect = self.loadSection(0)
|
||||
records, = struct.unpack('>H', sect[0x8:0x8+2])
|
||||
mobi_length, = struct.unpack('>L',sect[0x14:0x18])
|
||||
mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
|
||||
extra_data_flags = 0
|
||||
print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length)
|
||||
if (mobi_length >= 0xE4) and (mobi_version >= 5):
|
||||
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" %extra_data_flags
|
||||
|
||||
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
else:
|
||||
if crypto_type == 1:
|
||||
raise DrmException("cannot decode Mobipocket encryption type 1")
|
||||
if crypto_type != 2:
|
||||
raise DrmException("unknown encryption type: %d" % crypto_type)
|
||||
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("no PIDs found in this file")
|
||||
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
|
||||
if not found_key:
|
||||
raise DrmException("no key found. maybe the PID is incorrect")
|
||||
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
# 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, records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), 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:]
|
||||
#self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
|
||||
if self.num_sections > records+1:
|
||||
new_data += self.data_file[self.sections[records+1][0]:]
|
||||
self.data_file = new_data
|
||||
print "done"
|
||||
|
||||
def getResult(self):
|
||||
return self.data_file
|
||||
|
||||
if not __name__ == "__main__":
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class MobiDeDRM(FileTypePlugin):
|
||||
name = 'MobiDeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from secure Mobi files'
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = 'The Dark Reverser' # The author of this plugin
|
||||
version = (0, 1, 6) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
PID = self.site_customization
|
||||
data_file = file(path_to_ebook, 'rb').read()
|
||||
ar = PID.split(',')
|
||||
for i in ar:
|
||||
try:
|
||||
unlocked_file = DrmStripper(data_file, i).getResult()
|
||||
except DrmException:
|
||||
# ignore the error
|
||||
pass
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
return path_to_ebook
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter PID (separate multiple PIDs with comma)'
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(sys.argv)<4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> <PID>" % sys.argv[0]
|
||||
sys.exit(1)
|
||||
else:
|
||||
infile = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
pid = sys.argv[3]
|
||||
data_file = file(infile, 'rb').read()
|
||||
try:
|
||||
strippedFile = DrmStripper(data_file, pid)
|
||||
file(outfile, 'wb').write(strippedFile.getResult())
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/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)
|
||||
149
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/subasyncio.py
Normal file
149
Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/subasyncio.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/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()
|
||||
|
||||
Reference in New Issue
Block a user