]> xmof Git - DeDRM.git/commitdiff
tools v3.5
authorApprentice Alf <apprenticealf@gmail.com>
Thu, 17 Feb 2011 11:35:51 +0000 (11:35 +0000)
committerApprentice Alf <apprenticealf@gmail.com>
Thu, 5 Mar 2015 17:33:13 +0000 (17:33 +0000)
16 files changed:
Calibre_Plugins/k4mobidedrm_plugin.zip
Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py
Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py
DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt
KindleBooks_Tools/FindTopazEbooks.pyw [deleted file]
KindleBooks_Tools/KindleBooks/lib/k4mobidedrm.py
KindleBooks_Tools/KindleBooks/lib/mobidedrm.py
KindleBooks_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
KindleBooks_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
KindleBooks_Tools/MobiDeDRM.py [deleted file]
Mobi_Additional_Tools/lib/mobidedrm.py

index 14af1cc742060e9319345a819063fb8b86b51324..0e5c337ee882fdd3f2e0f1e62a65e48ec263b8c0 100644 (file)
Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ
index 6d37a5bc0b25da9145b4a9e54d12834ef77a2474..0255a3c84a02c0763d3af41b87191c35228bffb5 100644 (file)
@@ -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
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):
index 1eef22d0e00b131a86f116eea7f323140e356fa0..d1feae25d6d885a404ad5bebc229f11801e4e1ce 100644 (file)
@@ -24,7 +24,7 @@
        <key>CFBundleExecutable</key>
        <string>droplet</string>
        <key>CFBundleGetInfoString</key>
-       <string>DeDRM 2.2, Copyright © 2010–2011 by Apprentice Alf and others.</string>
+       <string>DeDRM 2.3, Copyright © 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.2</string>
+       <string>2.3</string>
        <key>CFBundleSignature</key>
        <string>dplt</string>
        <key>LSMinimumSystemVersion</key>
index 6d37a5bc0b25da9145b4a9e54d12834ef77a2474..0255a3c84a02c0763d3af41b87191c35228bffb5 100644 (file)
@@ -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
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):
index 6d37a5bc0b25da9145b4a9e54d12834ef77a2474..0255a3c84a02c0763d3af41b87191c35228bffb5 100644 (file)
@@ -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
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):
index 8e1cfea5b321dc5a3782e88a0a953e1bd93c1a43..111d2b955a304e9dd39b92345b065b4b1f6e8cd2 100644 (file)
@@ -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 (file)
index 6a0df30..0000000
+++ /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
index 6d37a5bc0b25da9145b4a9e54d12834ef77a2474..0255a3c84a02c0763d3af41b87191c35228bffb5 100644 (file)
@@ -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
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):
index 22663296426a03fdf63ff1a6ca0463c69e40d804..183432cc82b3b540d56227f4b707155cd648e95a 100644 (file)
@@ -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 
 #         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 __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")
 
-    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
+        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)
-
-        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)
+        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)
 
-        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)
-
-        # 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"
+            # 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
 
 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 <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
+        print "    %s <infile> <outfile> <PID>" % 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 (file)
index 2266329..0000000
+++ /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 <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())
index 22663296426a03fdf63ff1a6ca0463c69e40d804..ec756b91828fe1db33f9d95bc98ffceb12694b28 100644 (file)
@@ -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):