mobidedrm 0.15

This commit is contained in:
Apprentice Alf 2010-03-17 10:52:52 +00:00
parent 37696e1495
commit 6c70a073d9
3 changed files with 257 additions and 179 deletions

View file

@ -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

View file

@ -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

View file

@ -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 <infile> <outfile> <PID>" % 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)