mirror of
https://github.com/noDRM/DeDRM_tools
synced 2024-12-28 22:24:27 +01:00
tools v3.5
This commit is contained in:
parent
2bedd75005
commit
ac9cdb1e98
16 changed files with 189 additions and 932 deletions
Binary file not shown.
|
@ -29,7 +29,7 @@ from __future__ import with_statement
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.3'
|
__version__ = '2.4'
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
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.'
|
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
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
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
|
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
|
on_import = True # Run this plugin during the import
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||||
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>droplet</string>
|
<string>droplet</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<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>
|
<key>CFBundleIconFile</key>
|
||||||
<string>droplet</string>
|
<string>droplet</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.2</string>
|
<string>2.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>dplt</string>
|
<string>dplt</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|
|
@ -29,7 +29,7 @@ from __future__ import with_statement
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.3'
|
__version__ = '2.4'
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
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.'
|
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
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
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
|
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
|
on_import = True # Run this plugin during the import
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||||
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
|
@ -29,7 +29,7 @@ from __future__ import with_statement
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.3'
|
__version__ = '2.4'
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
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.'
|
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
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
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
|
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
|
on_import = True # Run this plugin during the import
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||||
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
|
@ -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.
|
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.
|
||||||
|
|
|
@ -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())
|
|
|
@ -29,7 +29,7 @@ from __future__ import with_statement
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
# and import that ZIP into Calibre using its plugin configuration GUI.
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.3'
|
__version__ = '2.4'
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
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.'
|
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
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
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
|
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
|
on_import = True # Run this plugin during the import
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||||
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
# 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
|
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||||
# only partially successful. Closer examination of lots of sample
|
# 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
|
# are not encrypted, but it turns out that the multibyte entries
|
||||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||||
# This knowledge leads to a simplification of the test for the
|
# This knowledge leads to a simplification of the test for the
|
||||||
|
@ -39,17 +39,13 @@
|
||||||
# Removed the disabled Calibre plug-in code
|
# Removed the disabled Calibre plug-in code
|
||||||
# Permit use of 8-digit PIDs
|
# Permit use of 8-digit PIDs
|
||||||
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
|
# 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.20 - Corretion: 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'
|
__version__ = '0.20'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
def __init__(self, stream):
|
||||||
|
@ -59,20 +55,10 @@ class Unbuffered:
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# MobiBook Utility Routines
|
|
||||||
#
|
|
||||||
|
|
||||||
# Implementation of Pukall Cipher 1
|
# Implementation of Pukall Cipher 1
|
||||||
def PC1(key, src, decryption=True):
|
def PC1(key, src, decryption=True):
|
||||||
sum1 = 0;
|
sum1 = 0;
|
||||||
|
@ -84,6 +70,7 @@ def PC1(key, src, decryption=True):
|
||||||
wkey = []
|
wkey = []
|
||||||
for i in xrange(8):
|
for i in xrange(8):
|
||||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||||
|
|
||||||
dst = ""
|
dst = ""
|
||||||
for i in xrange(len(src)):
|
for i in xrange(len(src)):
|
||||||
temp1 = 0;
|
temp1 = 0;
|
||||||
|
@ -144,9 +131,7 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||||
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||||
return num
|
return num
|
||||||
|
|
||||||
|
class DrmStripper:
|
||||||
|
|
||||||
class MobiBook:
|
|
||||||
def loadSection(self, section):
|
def loadSection(self, section):
|
||||||
if (section + 1 == self.num_sections):
|
if (section + 1 == self.num_sections):
|
||||||
endoff = len(self.data_file)
|
endoff = len(self.data_file)
|
||||||
|
@ -155,104 +140,6 @@ class MobiBook:
|
||||||
off = self.sections[section][0]
|
off = self.sections[section][0]
|
||||||
return self.data_file[off:endoff]
|
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):
|
def patch(self, off, new):
|
||||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(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
|
assert off + in_off + len(new) <= endoff
|
||||||
self.patch(off + in_off, new)
|
self.patch(off + in_off, new)
|
||||||
|
|
||||||
def parseDRM(self, data, count, pidlist):
|
def parseDRM(self, data, count, pid):
|
||||||
found_key = None
|
pid = pid.ljust(16,'\0')
|
||||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||||
for pid in pidlist:
|
temp_key = PC1(keyvec1, pid, False)
|
||||||
bigpid = pid.ljust(16,'\0')
|
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||||
temp_key = PC1(keyvec1, bigpid, False)
|
found_key = None
|
||||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
for i in xrange(count):
|
||||||
found_key = None
|
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||||
for i in xrange(count):
|
cookie = PC1(temp_key, cookie)
|
||||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||||
if cksum == temp_key_sum:
|
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
|
||||||
cookie = PC1(temp_key, cookie)
|
found_key = finalkey
|
||||||
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
|
break
|
||||||
if not found_key:
|
if not found_key:
|
||||||
# Then try the default encoding that doesn't require a PID
|
# Then try the default encoding that doesn't require a PID
|
||||||
pid = "00000000"
|
|
||||||
temp_key = keyvec1
|
temp_key = keyvec1
|
||||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||||
for i in xrange(count):
|
for i in xrange(count):
|
||||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||||
if cksum == temp_key_sum:
|
cookie = PC1(temp_key, cookie)
|
||||||
cookie = PC1(temp_key, cookie)
|
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
if verification == ver and cksum == temp_key_sum:
|
||||||
if verification == ver:
|
found_key = finalkey
|
||||||
found_key = finalkey
|
break
|
||||||
break
|
return found_key
|
||||||
return [found_key,pid]
|
|
||||||
|
|
||||||
def processBook(self, pidlist):
|
def __init__(self, data_file, pid):
|
||||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
if len(pid)==10:
|
||||||
print 'Crypto Type is: ', crypto_type
|
if checksumPid(pid[0:-2]) != pid:
|
||||||
self.crypto_type = crypto_type
|
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:
|
if crypto_type == 0:
|
||||||
print "This book is not encrypted."
|
print "This book is not encrypted."
|
||||||
return self.data_file
|
else:
|
||||||
if crypto_type != 2 and crypto_type != 1:
|
if crypto_type == 1:
|
||||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
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
|
# 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:
|
if drm_count == 0:
|
||||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
raise DrmException("no PIDs found in this file")
|
||||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
|
||||||
if not found_key:
|
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
|
# kill the drm keys
|
||||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||||
# kill the drm pointers
|
# kill the drm pointers
|
||||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||||
|
# clear the crypto type
|
||||||
if pid=="00000000":
|
self.patchSection(0, "\0" * 2, 0xC)
|
||||||
print "File has default encryption, no specific PID."
|
|
||||||
else:
|
|
||||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
|
||||||
|
|
||||||
# clear the crypto type
|
# decrypt sections
|
||||||
self.patchSection(0, "\0" * 2, 0xC)
|
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
|
def getResult(self):
|
||||||
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
|
return self.data_file
|
||||||
|
|
||||||
def getUnencryptedBook(infile,pid):
|
def getUnencryptedBook(infile,pid):
|
||||||
if not os.path.isfile(infile):
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
raise DrmException('Input File Not Found')
|
data_file = file(infile, 'rb').read()
|
||||||
book = MobiBook(infile)
|
strippedFile = DrmStripper(data_file, pid)
|
||||||
return book.processBook([pid])
|
return strippedFile.getResult()
|
||||||
|
|
||||||
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):
|
def main(argv=sys.argv):
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
print ('MobiDeDrm v%(__version__)s. '
|
print ('MobiDeDrm v%(__version__)s. '
|
||||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
'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 "Removes protection from Mobipocket books"
|
||||||
print "Usage:"
|
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
|
return 1
|
||||||
else:
|
else:
|
||||||
infile = argv[1]
|
infile = argv[1]
|
||||||
outfile = argv[2]
|
outfile = argv[2]
|
||||||
if len(argv) is 4:
|
pid = argv[3]
|
||||||
pidlist = argv[3].split(',')
|
|
||||||
else:
|
|
||||||
pidlist = {}
|
|
||||||
try:
|
try:
|
||||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
stripped_file = getUnencryptedBook(infile, pid)
|
||||||
file(outfile, 'wb').write(stripped_file)
|
file(outfile, 'wb').write(stripped_file)
|
||||||
except DrmException, e:
|
except DrmException, e:
|
||||||
print "Error: %s" % e
|
print "Error: %s" % e
|
||||||
|
|
|
@ -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())
|
|
|
@ -46,8 +46,9 @@
|
||||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
# 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
|
import sys
|
||||||
|
|
||||||
|
@ -207,19 +208,16 @@ class MobiBook:
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
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
|
# reset the text to speech flag and clipping limit, if present
|
||||||
if type == 401 and size == 9:
|
if type == 401 and size == 9:
|
||||||
# set clipping limit to 100%
|
# set clipping limit to 100%
|
||||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\144"
|
|
||||||
elif type == 404 and size == 9:
|
elif type == 404 and size == 9:
|
||||||
# make sure text to speech is enabled
|
# make sure text to speech is enabled
|
||||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||||
content = "\0"
|
# print type, size, content, content.encode('hex')
|
||||||
else:
|
|
||||||
content = exth[pos + 8: pos + size]
|
|
||||||
#print type, size, content
|
|
||||||
self.meta_array[type] = content
|
|
||||||
pos += size
|
pos += size
|
||||||
except:
|
except:
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
@ -244,13 +242,14 @@ class MobiBook:
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
token = ''
|
||||||
# The last character of the 209 data points to the record with the token.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
for i in xrange(len(data)):
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
if ord(data[i]) != 0:
|
for i in xrange(0,len(data),5):
|
||||||
if self.meta_array[ord(data[i])] != None:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
token = self.meta_array[ord(data[i])]
|
sval = self.meta_array.get(val,'')
|
||||||
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
|
|
Loading…
Reference in a new issue