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):