From 86357531a58c329fbf9291cbb9a7f0e27299dac8 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Sun, 8 Mar 2009 21:28:23 +0000 Subject: [PATCH] mobidedrm 0.13 --- Kindle_Mobi_Tools/lib/mobidedrm.py | 458 +++++++++++++++-------------- 1 file changed, 230 insertions(+), 228 deletions(-) diff --git a/Kindle_Mobi_Tools/lib/mobidedrm.py b/Kindle_Mobi_Tools/lib/mobidedrm.py index 02a2db3..0322018 100644 --- a/Kindle_Mobi_Tools/lib/mobidedrm.py +++ b/Kindle_Mobi_Tools/lib/mobidedrm.py @@ -26,263 +26,265 @@ # 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. -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) +__version__ = '0.13' import sys -sys.stdout=Unbuffered(sys.stdout) +import struct +import binascii -import struct,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; - 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])) + 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 + 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 + 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 + 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 - 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 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 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 - 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: - 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") - - # 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 + # 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 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, 2) # 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, 1, 2) # 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): - 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 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.12. 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)" - 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) + 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