mobidedrm 0.13

This commit is contained in:
Apprentice Alf 2009-03-08 21:28:23 +00:00
parent 8e7d2657a4
commit 86357531a5

View File

@ -26,263 +26,265 @@
# Also now handles encrypted files that don't need a specific PID. # Also now handles encrypted files that don't need a specific PID.
# 0.11 - use autoflushed stdout and proper return values # 0.11 - use autoflushed stdout and proper return values
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors # 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: __version__ = '0.13'
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)
import sys 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): class DrmException(Exception):
pass pass
#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;
sum2 = 0; sum2 = 0;
keyXorVal = 0; keyXorVal = 0;
if len(key)!=16: if len(key)!=16:
print "Bad key length!" print "Bad key length!"
return None return None
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;
byteXorVal = 0; byteXorVal = 0;
for j in xrange(8): for j in xrange(8):
temp1 ^= wkey[j] temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1 sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2 byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i]) curByte = ord(src[i])
if not decryption: if not decryption:
keyXorVal = curByte * 257; keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption: if decryption:
keyXorVal = curByte * 257; keyXorVal = curByte * 257;
for j in xrange(8): for j in xrange(8):
wkey[j] ^= keyXorVal; wkey[j] ^= keyXorVal;
dst+=chr(curByte) dst+=chr(curByte)
return dst return dst
def checksumPid(s): def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16) crc = crc ^ (crc >> 16)
res = s res = s
l = len(letters) l = len(letters)
for i in (0,1): for i in (0,1):
b = crc & 0xff b = crc & 0xff
pos = (b // l) ^ (b % l) pos = (b // l) ^ (b % l)
res += letters[pos%l] res += letters[pos%l]
crc >>= 8 crc >>= 8
return res return res
def getSizeOfTrailingDataEntries(ptr, size, flags): def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size): def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0 bitpos, result = 0, 0
if size <= 0: if size <= 0:
return result return result
while True: while True:
v = ord(ptr[size-1]) v = ord(ptr[size-1])
result |= (v & 0x7F) << bitpos result |= (v & 0x7F) << bitpos
bitpos += 7 bitpos += 7
size -= 1 size -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
return result return result
num = 0 num = 0
testflags = flags >> 1 testflags = flags >> 1
while testflags: while testflags:
if testflags & 1: if testflags & 1:
num += getSizeOfTrailingDataEntry(ptr, size - num) num += getSizeOfTrailingDataEntry(ptr, size - num)
testflags >>= 1 testflags >>= 1
if flags & 1: if flags & 1:
num += (ord(ptr[size - num - 1]) & 0x3) + 1 num += (ord(ptr[size - num - 1]) & 0x3) + 1
return num return num
class DrmStripper: class DrmStripper:
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)
else: else:
endoff = self.sections[section + 1][0] endoff = self.sections[section + 1][0]
off = self.sections[section][0] off = self.sections[section][0]
return self.data_file[off:endoff] return self.data_file[off:endoff]
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):]
def patchSection(self, section, new, in_off = 0): def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections): if (section + 1 == self.num_sections):
endoff = len(self.data_file) endoff = len(self.data_file)
else: else:
endoff = self.sections[section + 1][0] endoff = self.sections[section + 1][0]
off = self.sections[section][0] off = self.sections[section][0]
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, pid): def parseDRM(self, data, count, pid):
pid = pid.ljust(16,'\0') 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"
temp_key = PC1(keyvec1, pid, False) temp_key = PC1(keyvec1, pid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff temp_key_sum = sum(map(ord,temp_key)) & 0xff
found_key = None found_key = None
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])
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 and (flags & 0x1F) == 1: if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
found_key = finalkey found_key = finalkey
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
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])
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 and cksum == temp_key_sum:
found_key = finalkey found_key = finalkey
break break
return found_key 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: self.sections = []
raise DrmException("invalid PID checksum") for i in xrange(self.num_sections):
pid = pid[0:-2] 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.data_file = data_file sect = self.loadSection(0)
header = data_file[0:72] records, = struct.unpack('>H', sect[0x8:0x8+2])
if header[0x3C:0x3C+8] != 'BOOKMOBI': mobi_length, = struct.unpack('>L',sect[0x14:0x18])
raise DrmException("invalid file format") mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
self.num_sections, = struct.unpack('>H', data_file[76:78]) 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
self.sections = [] crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
for i in xrange(self.num_sections): if crypto_type == 0:
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) print "This book is not encrypted."
flags, val = a1, a2<<16|a3<<8|a4 else:
self.sections.append( (offset, flags, val) ) if crypto_type == 1:
raise DrmException("cannot decode Mobipocket encryption type 1")
if crypto_type != 2:
raise DrmException("unknown encryption type: %d" % crypto_type)
sect = self.loadSection(0) # calculate the keys
records, = struct.unpack('>H', sect[0x8:0x8+2]) drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
mobi_length, = struct.unpack('>L',sect[0x14:0x18]) if drm_count == 0:
mobi_version, = struct.unpack('>L',sect[0x68:0x6C]) raise DrmException("no PIDs found in this file")
extra_data_flags = 0 found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
print "MOBI header length = %d" %mobi_length if not found_key:
print "MOBI header version = %d" %mobi_version raise DrmException("no key found. maybe the PID is incorrect")
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
# 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)
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) # decrypt sections
if crypto_type == 0: print "Decrypting. Please wait...",
print "This book is not encrypted." for i in xrange(1, records+1):
else: data = self.loadSection(i)
if crypto_type == 1: extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
raise DrmException("cannot decode Mobipocket encryption type 1") # print "record %d, extra_size %d" %(i,extra_size)
if crypto_type != 2: self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
raise DrmException("unknown encryption type: %d" % crypto_type) print "done"
# calculate the keys def getResult(self):
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16]) return self.data_file
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
if not __name__ == "__main__": 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 def run(self, path_to_ebook):
description = 'Removes DRM from secure Mobi files' from calibre.gui2 import is_ok_to_use_qt
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on from PyQt4.Qt import QMessageBox
author = 'The Dark Reverser' # The author of this plugin PID = self.site_customization
version = (0, 1, 2) # The version number of this plugin data_file = file(path_to_ebook, 'rb').read()
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to ar = PID.split(',')
on_import = True # Run this plugin during the import 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):
def run(self, path_to_ebook): return 'Enter PID (separate multiple PIDs with comma)'
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)'
if __name__ == "__main__": if __name__ == "__main__":
print "MobiDeDrm v0.12. Copyright (c) 2008 The Dark Reverser" sys.stdout=Unbuffered(sys.stdout)
if len(sys.argv)<4: print ('MobiDeDrm v%(__version__)s. '
print "Removes protection from Mobipocket books" 'Copyright 2008-2010 The Dark Reverser.' % globals())
print "Usage:" if len(sys.argv)<4:
print " mobidedrm infile.mobi outfile.mobi (PID)" print "Removes protection from Mobipocket books"
sys.exit(1) print "Usage:"
else: print " %s <infile> <outfile> <PID>" % sys.argv[0]
infile = sys.argv[1] sys.exit(1)
outfile = sys.argv[2] else:
pid = sys.argv[3] infile = sys.argv[1]
data_file = file(infile, 'rb').read() outfile = sys.argv[2]
try: pid = sys.argv[3]
strippedFile = DrmStripper(data_file, pid) data_file = file(infile, 'rb').read()
file(outfile, 'wb').write(strippedFile.getResult()) try:
except DrmException, e: strippedFile = DrmStripper(data_file, pid)
print "Error: %s" % e file(outfile, 'wb').write(strippedFile.getResult())
sys.exit(1) except DrmException, e:
sys.exit(0) print "Error: %s" % e
sys.exit(1)
sys.exit(0)