diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip index 14af1cc..0e5c337 100644 Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py b/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py index 6d37a5b..0255a3c 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py @@ -29,7 +29,7 @@ from __future__ import with_statement # and import that ZIP into Calibre using its plugin configuration GUI. -__version__ = '2.3' +__version__ = '2.4' class Unbuffered: def __init__(self, stream): @@ -250,7 +250,7 @@ if not __name__ == "__main__" and inCalibre: 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, 3) # The version number of this plugin + version = (0, 2, 4) # 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 diff --git a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py index 2266329..ec756b9 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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): diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index 1eef22d..d1feae2 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,7 +24,7 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM 2.2, Copyright © 2010–2011 by Apprentice Alf and others. + DeDRM 2.3, Copyright © 2010–2011 by Apprentice Alf and others. CFBundleIconFile droplet CFBundleInfoDictionaryVersion @@ -34,7 +34,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.2 + 2.3 CFBundleSignature dplt LSMinimumSystemVersion diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index 6d37a5b..0255a3c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -29,7 +29,7 @@ from __future__ import with_statement # and import that ZIP into Calibre using its plugin configuration GUI. -__version__ = '2.3' +__version__ = '2.4' class Unbuffered: def __init__(self, stream): @@ -250,7 +250,7 @@ if not __name__ == "__main__" and inCalibre: 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, 3) # The version number of this plugin + version = (0, 2, 4) # 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 diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py index 2266329..ec756b9 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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): diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py index 6d37a5b..0255a3c 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py @@ -29,7 +29,7 @@ from __future__ import with_statement # and import that ZIP into Calibre using its plugin configuration GUI. -__version__ = '2.3' +__version__ = '2.4' class Unbuffered: def __init__(self, stream): @@ -250,7 +250,7 @@ if not __name__ == "__main__" and inCalibre: 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, 3) # The version number of this plugin + version = (0, 2, 4) # 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 diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py index 2266329..ec756b9 100644 --- a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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): diff --git a/DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt b/DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt index 8e1cfea..111d2b9 100644 --- a/DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt +++ b/DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt @@ -1,4 +1,4 @@ -ReadMe_DeDRM_WinApp_v1.2 +ReadMe_DeDRM_WinApp_v1.5 ----------------------- 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. diff --git a/KindleBooks_Tools/FindTopazEbooks.pyw b/KindleBooks_Tools/FindTopazEbooks.pyw deleted file mode 100644 index 6a0df30..0000000 --- a/KindleBooks_Tools/FindTopazEbooks.pyw +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/KindleBooks_Tools/KindleBooks/lib/k4mobidedrm.py b/KindleBooks_Tools/KindleBooks/lib/k4mobidedrm.py index 6d37a5b..0255a3c 100644 --- a/KindleBooks_Tools/KindleBooks/lib/k4mobidedrm.py +++ b/KindleBooks_Tools/KindleBooks/lib/k4mobidedrm.py @@ -29,7 +29,7 @@ from __future__ import with_statement # and import that ZIP into Calibre using its plugin configuration GUI. -__version__ = '2.3' +__version__ = '2.4' class Unbuffered: def __init__(self, stream): @@ -250,7 +250,7 @@ if not __name__ == "__main__" and inCalibre: 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, 3) # The version number of this plugin + version = (0, 2, 4) # 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 diff --git a/KindleBooks_Tools/KindleBooks/lib/mobidedrm.py b/KindleBooks_Tools/KindleBooks/lib/mobidedrm.py index 2266329..ec756b9 100644 --- a/KindleBooks_Tools/KindleBooks/lib/mobidedrm.py +++ b/KindleBooks_Tools/KindleBooks/lib/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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): diff --git a/KindleBooks_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py b/KindleBooks_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py index 2266329..ec756b9 100644 --- a/KindleBooks_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py +++ b/KindleBooks_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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): diff --git a/KindleBooks_Tools/Kindle_4_PC_Unswindle/mobidedrm.py b/KindleBooks_Tools/Kindle_4_PC_Unswindle/mobidedrm.py index 2266329..183432c 100644 --- a/KindleBooks_Tools/Kindle_4_PC_Unswindle/mobidedrm.py +++ b/KindleBooks_Tools/Kindle_4_PC_Unswindle/mobidedrm.py @@ -24,7 +24,7 @@ # 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 +# 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 @@ -39,17 +39,13 @@ # 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% +# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file. -__version__ = '0.26' +__version__ = '0.20' import sys +import struct +import binascii class Unbuffered: def __init__(self, stream): @@ -59,20 +55,10 @@ class Unbuffered: 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; @@ -84,6 +70,7 @@ def PC1(key, src, decryption=True): 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; @@ -144,9 +131,7 @@ def getSizeOfTrailingDataEntries(ptr, size, flags): num += (ord(ptr[size - num - 1]) & 0x3) + 1 return num - - -class MobiBook: +class DrmStripper: def loadSection(self, section): if (section + 1 == self.num_sections): endoff = len(self.data_file) @@ -155,104 +140,6 @@ class MobiBook: 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):] @@ -265,136 +152,134 @@ class MobiBook: assert off + in_off + len(new) <= endoff self.patch(off + in_off, new) - def parseDRM(self, data, count, pidlist): - found_key = None + 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" - 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: + 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 - 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] + 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 processBook(self, pidlist): - crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) - print 'Crypto Type is: ', crypto_type - self.crypto_type = crypto_type + def __init__(self, data_file, pid): + if len(pid)==10: + if checksumPid(pid[0:-2]) != pid: + raise DrmException("invalid PID checksum") + pid = pid[0:-2] + elif len(pid)==8: + print "PID without checksum given. With checksum PID is "+checksumPid(pid) + else: + raise DrmException("Invalid PID length") + + 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 + if 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. + extra_data_flags &= 0xFFFE + + crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) 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) + 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) - 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]) + drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', 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) + 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. Most likely the correct PID has not been given.") + 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) - - 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) - # 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" - # 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" + def getResult(self): 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) + sys.stdout=Unbuffered(sys.stdout) + data_file = file(infile, 'rb').read() + strippedFile = DrmStripper(data_file, pid) + return strippedFile.getResult() def main(argv=sys.argv): + sys.stdout=Unbuffered(sys.stdout) print ('MobiDeDrm v%(__version__)s. ' 'Copyright 2008-2010 The Dark Reverser.' % globals()) - if len(argv)<3 or len(argv)>4: + if len(argv)<4: print "Removes protection from Mobipocket books" print "Usage:" - print " %s []" % sys.argv[0] + print " %s " % sys.argv[0] return 1 else: infile = argv[1] outfile = argv[2] - if len(argv) is 4: - pidlist = argv[3].split(',') - else: - pidlist = {} + pid = argv[3] try: - stripped_file = getUnencryptedBookWithList(infile, pidlist) + stripped_file = getUnencryptedBook(infile, pid) file(outfile, 'wb').write(stripped_file) except DrmException, e: print "Error: %s" % e diff --git a/KindleBooks_Tools/MobiDeDRM.py b/KindleBooks_Tools/MobiDeDRM.py deleted file mode 100644 index 2266329..0000000 --- a/KindleBooks_Tools/MobiDeDRM.py +++ /dev/null @@ -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 []" % 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()) diff --git a/Mobi_Additional_Tools/lib/mobidedrm.py b/Mobi_Additional_Tools/lib/mobidedrm.py index 2266329..ec756b9 100644 --- a/Mobi_Additional_Tools/lib/mobidedrm.py +++ b/Mobi_Additional_Tools/lib/mobidedrm.py @@ -46,8 +46,9 @@ # 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!) -__version__ = '0.26' +__version__ = '0.27' import sys @@ -207,19 +208,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 = {} @@ -244,13 +242,14 @@ class MobiBook: 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])] + token = '' + # 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):