diff --git a/Kindle_Mobi_Tools/lib/mobidedrm.py b/Kindle_Mobi_Tools/lib/mobidedrm.py index 59e749d..0565356 100644 --- a/Kindle_Mobi_Tools/lib/mobidedrm.py +++ b/Kindle_Mobi_Tools/lib/mobidedrm.py @@ -37,8 +37,9 @@ # in utf8 file are encrypted. (Although neither kind gets compressed.) # This knowledge leads to a simplification of the test for the # trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'hearbeat', and is also quicker for long files. -__version__ = '0.14' +__version__ = '0.15' import sys import struct @@ -196,8 +197,7 @@ class DrmStripper: mobi_length, = struct.unpack('>L',sect[0x14:0x18]) mobi_version, = struct.unpack('>L',sect[0x68:0x6C]) extra_data_flags = 0 - print "MOBI header length = %d" %mobi_length - print "MOBI header version = %d" %mobi_version + 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 @@ -227,13 +227,22 @@ class DrmStripper: self.patchSection(0, "\0" * 2, 0xC) # decrypt sections - print "Decrypting. Please wait...", + 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) - self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) - print "done" + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done." def getResult(self): return self.data_file @@ -246,7 +255,7 @@ if not __name__ == "__main__": description = 'Removes DRM from secure Mobi files' supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on author = 'The Dark Reverser' # The author of this plugin - version = (0, 1, 4) # The version number of this plugin + version = (0, 1, 5) # The version number of this plugin file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import diff --git a/Kindle_Mobi_Tools/unswindle/mobidedrm.py b/Kindle_Mobi_Tools/unswindle/mobidedrm.py index 59e749d..0565356 100644 --- a/Kindle_Mobi_Tools/unswindle/mobidedrm.py +++ b/Kindle_Mobi_Tools/unswindle/mobidedrm.py @@ -37,8 +37,9 @@ # in utf8 file are encrypted. (Although neither kind gets compressed.) # This knowledge leads to a simplification of the test for the # trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'hearbeat', and is also quicker for long files. -__version__ = '0.14' +__version__ = '0.15' import sys import struct @@ -196,8 +197,7 @@ class DrmStripper: mobi_length, = struct.unpack('>L',sect[0x14:0x18]) mobi_version, = struct.unpack('>L',sect[0x68:0x6C]) extra_data_flags = 0 - print "MOBI header length = %d" %mobi_length - print "MOBI header version = %d" %mobi_version + 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 @@ -227,13 +227,22 @@ class DrmStripper: self.patchSection(0, "\0" * 2, 0xC) # decrypt sections - print "Decrypting. Please wait...", + 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) - self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) - print "done" + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done." def getResult(self): return self.data_file @@ -246,7 +255,7 @@ if not __name__ == "__main__": description = 'Removes DRM from secure Mobi files' supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on author = 'The Dark Reverser' # The author of this plugin - version = (0, 1, 4) # The version number of this plugin + version = (0, 1, 5) # The version number of this plugin file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to on_import = True # Run this plugin during the import diff --git a/Macintosh_Applications/Mobipocket Unlocker.app/Contents/Resources/MobiDeDRM.py b/Macintosh_Applications/Mobipocket Unlocker.app/Contents/Resources/MobiDeDRM.py index 1f02cf9..0565356 100644 --- a/Macintosh_Applications/Mobipocket Unlocker.app/Contents/Resources/MobiDeDRM.py +++ b/Macintosh_Applications/Mobipocket Unlocker.app/Contents/Resources/MobiDeDRM.py @@ -21,13 +21,43 @@ # 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size # 0.08 - ...and also not in Mobi header version < 6 # 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! +# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre +# import filter it works when importing unencrypted files. +# Also now handles encrypted files that don't need a specific PID. +# 0.11 - use autoflushed stdout and proper return values +# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors +# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace +# and extra blank lines, converted CR/LF pairs at ends of each line, +# and other cosmetic fixes. +# 0.14 - Working out when the extra data flags are present has been problematic +# Versions 7 through 9 have tried to tweak the conditions, but have been +# only partially successful. Closer examination of lots of sample +# files reveals that a confusin has arisen because trailing data entries +# are not encrypted, but it turns out that the multibyte entries +# in utf8 file are encrypted. (Although neither kind gets compressed.) +# This knowledge leads to a simplification of the test for the +# trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'hearbeat', and is also quicker for long files. -import sys,struct,binascii +__version__ = '0.15' + +import sys +import struct +import binascii + +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) class DrmException(Exception): - pass + pass -#implementation of Pukall Cipher 1 +# Implementation of Pukall Cipher 1 def PC1(key, src, decryption=True): sum1 = 0; sum2 = 0; @@ -62,188 +92,218 @@ def PC1(key, src, decryption=True): 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 + 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 - if flags & 1: - num += (ord(ptr[size - num - 1]) & 0x3) + 1 - return num + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Multibyte data, if present, is included in the encryption, so + # we do not need to check the low bit. + # if flags & 1: + # num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num class DrmStripper: - def loadSection(self, section): - if (section + 1 == self.num_sections): - endoff = len(self.data_file) - else: - endoff = self.sections[section + 1][0] - off = self.sections[section][0] - return self.data_file[off:endoff] + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] - def patch(self, off, new): - self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + def 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 patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) - def parseDRM(self, data, count, pid): - pid = pid.ljust(16,'\0') - keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" - temp_key = PC1(keyvec1, pid, False) - temp_key_sum = sum(map(ord,temp_key)) & 0xff - found_key = None - for i in xrange(count): - verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) - cookie = PC1(temp_key, cookie) - ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) - if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: - found_key = finalkey - break - return found_key + def parseDRM(self, data, count, pid): + pid = pid.ljust(16,'\0') + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + temp_key = PC1(keyvec1, pid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: + found_key = finalkey + break + if not found_key: + # Then try the default encoding that doesn't require a PID + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum: + found_key = finalkey + break + return found_key + def __init__(self, data_file, pid): + if checksumPid(pid[0:-2]) != pid: + raise DrmException("invalid PID checksum") + pid = pid[0:-2] - def __init__(self, data_file, pid): + 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]) - if checksumPid(pid[0:-2]) != pid: - raise DrmException("invalid PID checksum") - pid = pid[0:-2] - - self.data_file = data_file - header = data_file[0:72] - if header[0x3C:0x3C+8] != 'BOOKMOBI': - raise DrmException("invalid file format") - self.num_sections, = struct.unpack('>H', data_file[76:78]) + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) - 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 - 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 length = %d" %mobi_length - print "MOBI header version = %d" %mobi_version - if (mobi_length >= 0xE4) and (mobi_version > 5): - extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4]) - print "Extra Data Flags = %d" %extra_data_flags + crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) + if crypto_type == 0: + print "This book is not encrypted." + else: + if crypto_type == 1: + raise DrmException("cannot decode Mobipocket encryption type 1") + if crypto_type != 2: + raise DrmException("unknown encryption type: %d" % crypto_type) + # calculate the keys + drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("no PIDs found in this file") + found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) + if not found_key: + raise DrmException("no key found. maybe the PID is incorrect") - crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) - if crypto_type == 0: - raise DrmException("it seems that this book isn't encrypted") - if crypto_type == 1: - raise DrmException("cannot decode Mobipocket encryption type 1") - if crypto_type != 2: - raise DrmException("unknown encryption type: %d" % crypto_type) + # kill the drm keys + self.patchSection(0, "\0" * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) - # calculate the keys - drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16]) - if drm_count == 0: - raise DrmException("no PIDs found in this file") - found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) - if not found_key: - raise DrmException("no key found. maybe the PID is incorrect") + # 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." - # kill the drm keys - self.patchSection(0, "\0" * drm_size, drm_ptr) - # kill the drm pointers - self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) - # clear the crypto type - self.patchSection(0, "\0" * 2, 0xC) - - # decrypt sections - print "Decrypting. Please wait...", - for i in xrange(1, records+1): - data = self.loadSection(i) - extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags) - # print "record %d, extra_size %d" %(i,extra_size) - self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) - print "done" - def getResult(self): - return self.data_file + def getResult(self): + return self.data_file if not __name__ == "__main__": - from calibre.customize import FileTypePlugin + from calibre.customize import FileTypePlugin - class MobiDeDRM(FileTypePlugin): + class MobiDeDRM(FileTypePlugin): + name = 'MobiDeDRM' # Name of the plugin + description = 'Removes DRM from secure Mobi files' + supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on + author = 'The Dark Reverser' # The author of this plugin + version = (0, 1, 5) # The version number of this plugin + file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to + on_import = True # Run this plugin during the import - name = 'MobiDeDRM' # Name of the plugin - description = 'Removes DRM from secure Mobi files' - supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on - author = 'The Dark Reverser' # The author of this plugin - version = (0, 0, 9) # The version number of this plugin - file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to - on_import = True # Run this plugin during the import + def run(self, path_to_ebook): + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + PID = self.site_customization + data_file = file(path_to_ebook, 'rb').read() + ar = PID.split(',') + for i in ar: + try: + unlocked_file = DrmStripper(data_file, i).getResult() + except DrmException: + # ignore the error + pass + else: + of = self.temporary_file('.mobi') + of.write(unlocked_file) + of.close() + return of.name + if is_ok_to_use_qt(): + d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook) + d.show() + d.raise_() + d.exec_() + return path_to_ebook - - def run(self, path_to_ebook): - of = self.temporary_file('.mobi') - PID = self.site_customization - data_file = file(path_to_ebook, 'rb').read() - ar = PID.split(',') - for i in ar: - try: - file(of.name, 'wb').write(DrmStripper(data_file, i).getResult()) - except DrmException: - # Hm, we should display an error dialog here. - # Dunno how though. - # Ignore the dirty hack behind the curtain. -# strexcept = 'echo exception: %s > /dev/tty' % e -# subprocess.call(strexcept,shell=True) - print i + ": not PID for book" - else: - return of.name - - def customization_help(self, gui=False): - return 'Enter PID (separate multiple PIDs with comma)' + def customization_help(self, gui=False): + return 'Enter PID (separate multiple PIDs with comma)' if __name__ == "__main__": - print "MobiDeDrm v0.09. Copyright (c) 2008 The Dark Reverser" - if len(sys.argv)<4: - print "Removes protection from Mobipocket books" - print "Usage:" - print " mobidedrm infile.mobi outfile.mobi PID" - else: - infile = sys.argv[1] - outfile = sys.argv[2] - pid = sys.argv[3] - data_file = file(infile, 'rb').read() - try: - file(outfile, 'wb').write(DrmStripper(data_file, pid).getResult()) - except DrmException, e: - print "Error: %s" % e + sys.stdout=Unbuffered(sys.stdout) + print ('MobiDeDrm v%(__version__)s. ' + 'Copyright 2008-2010 The Dark Reverser.' % globals()) + if len(sys.argv)<4: + print "Removes protection from Mobipocket books" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + infile = sys.argv[1] + outfile = sys.argv[2] + pid = sys.argv[3] + data_file = file(infile, 'rb').read() + try: + strippedFile = DrmStripper(data_file, pid) + file(outfile, 'wb').write(strippedFile.getResult()) + except DrmException, e: + print "Error: %s" % e + sys.exit(1) + sys.exit(0) \ No newline at end of file