@ -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 confusi n has arisen because trailing data entries
# files reveals that a confusi o n 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,13 +39,19 @@
# 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 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
# 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%
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
# 0.28 - slight additional changes to metadata token generation (None -> '')
__version__ = ' 0.20 '
__version__ = ' 0.2 8 '
import sys
import sys
import struct
import binascii
class Unbuffered :
class Unbuffered :
def __init__ ( self , stream ) :
def __init__ ( self , stream ) :
@ -55,10 +61,20 @@ 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 ;
@ -70,7 +86,6 @@ 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 ;
@ -131,7 +146,9 @@ 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 )
@ -140,6 +157,101 @@ class DrmStripper:
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 ] )
content = exth [ pos + 8 : pos + size ]
self . meta_array [ type ] = content
# 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 )
elif type == 404 and size == 9 :
# make sure text to speech is enabled
self . patchSection ( 0 , " \0 " , 16 + self . mobi_length + pos + 8 )
# print type, size, content, content.encode('hex')
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 = ' '
token = ' '
if 209 in self . meta_array :
rec209 = self . meta_array [ 209 ]
data = rec209
# The 209 data comes in five byte groups. Interpret the last four bytes
# of each group as a big endian unsigned integer to get a key value
# if that key exists in the meta_array, append its contents to the token
for i in xrange ( 0 , len ( data ) , 5 ) :
val , = struct . unpack ( ' >I ' , data [ i + 1 : i + 5 ] )
sval = self . meta_array . get ( val , ' ' )
token + = sval
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 ) : ]
@ -152,134 +264,136 @@ class DrmStripper:
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 , pidlist ) :
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
found_key = None
for i in xrange ( count ) :
keyvec1 = " \x72 \x38 \x33 \xB0 \xB4 \xF2 \xE3 \xCA \xDF \x09 \x01 \xD6 \xE2 \xE0 \x3F \x96 "
verification , size , type , cksum , cookie = struct . unpack ( ' >LLLBxxx32s ' , data [ i * 0x30 : i * 0x30 + 0x30 ] )
for pid in pidlist :
cookie = PC1 ( temp_key , cookie )
bigpid = pid . ljust ( 16 , ' \0 ' )
ver , flags , finalkey , expiry , expiry2 = struct . unpack ( ' >LL16sLL ' , cookie )
temp_key = PC1 ( keyvec1 , bigpid , False )
if verification == ver and cksum == temp_key_sum and ( flags & 0x1F ) == 1 :
temp_key_sum = sum ( map ( ord , temp_key ) ) & 0xff
found_key = finalkey
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
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 ] )
cookie = PC1 ( temp_key , cookie )
if cksum == temp_key_sum :
ver , flags , finalkey , expiry , expiry2 = struct . unpack ( ' >LL16sLL ' , cookie )
cookie = PC1 ( temp_key , cookie )
if verification == ver and cksum == temp_key_sum :
ver , flags , finalkey , expiry , expiry2 = struct . unpack ( ' >LL16sLL ' , cookie )
found_key = finalkey
if verification == ver :
break
found_key = finalkey
return found_key
break
return [ found_key , pid ]
def __init__ ( self , data_file , pid ) :
if len ( pid ) == 10 :
if checksumPid ( pid [ 0 : - 2 ] ) != pid :
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 ] )
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 :
if crypto_type == 0 :
print " This book is not encrypted. "
print " This book is not encrypted. "
else :
return self . data_file
if crypto_type == 1 :
if crypto_type != 2 and crypto_type != 1 :
raise DrmException ( " cannot decode Mobipocket encryption type 1 " )
raise DrmException ( " Cannot decode unknown Mobipocket encryption type %d " % crypto_type )
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 ' , sect [ 0xA8 : 0xA8 + 16 ] )
drm_ptr , drm_count , drm_size , drm_flags = struct . unpack ( ' >LLLL ' , self . sect [ 0xA8 : 0xA8 + 16 ] )
if drm_count == 0 :
if drm_count == 0 :
raise DrmException ( " no PIDs found in this file " )
raise DrmException ( " Not yet initialised with PID. Must be opened with Mobipocket Reader first. " )
found_key = self . parseDRM ( sect [ drm_ptr : drm_ptr + drm_size ] , drm_count , pid )
found_key , pid = self . parseDRM ( self . sect [ drm_ptr : drm_ptr + drm_size ] , drm_count , good pids )
if not found_key :
if not found_key :
raise DrmException ( " no key found. maybe the PID is incorrect " )
raise DrmException ( " No key found. Most likely the correct PID has not been given. " )
# 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
self . patchSection ( 0 , " \0 " * 2 , 0xC )
if pid == " 00000000 " :
print " File has default encryption, no specific PID. "
# decrypt sections
else :
print " Decrypting. Please wait . . . " ,
print " File is encoded with PID " + checksumPid ( pid ) + " . "
new_data = self . data_file [ : self . sections [ 1 ] [ 0 ] ]
for i in xrange ( 1 , records + 1 ) :
# clear the crypto type
data = self . loadSection ( i )
self . patchSection ( 0 , " \0 " * 2 , 0xC )
extra_size = getSizeOfTrailingDataEntries ( data , len ( data ) , extra_data_flags )
if i % 100 == 0 :
# decrypt sections
print " . " ,
print " Decrypting. Please wait . . . " ,
# print "record %d, extra_size %d" %(i,extra_size)
new_data = self . data_file [ : self . sections [ 1 ] [ 0 ] ]
new_data + = PC1 ( found_key , data [ 0 : len ( data ) - extra_size ] )
for i in xrange ( 1 , self . records + 1 ) :
if extra_size > 0 :
data = self . loadSection ( i )
new_data + = data [ - extra_size : ]
extra_size = getSizeOfTrailingDataEntries ( data , len ( data ) , self . extra_data_flags )
#self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
if i % 100 == 0 :
if self . num_sections > records + 1 :
print " . " ,
new_data + = self . data_file [ self . sections [ records + 1 ] [ 0 ] : ]
# print "record %d, extra_size %d" %(i,extra_size)
self . data_file = new_data
new_data + = PC1 ( found_key , data [ 0 : len ( data ) - extra_size ] )
print " done "
if extra_size > 0 :
new_data + = data [ - extra_size : ]
def getResult ( self ) :
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 ) :
sys . stdout = Unbuffered ( sys . stdout )
if not os . path . isfile ( infile ) :
data_file = file ( infile , ' rb ' ) . read ( )
raise DrmException ( ' Input File Not Found ' )
strippedFile = DrmStripper ( data_file , pid )
book = MobiBook ( infile )
return strippedFile . getResult ( )
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 ) :
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 ) < 4 :
if len ( argv ) < 3 or len ( argv ) > 4:
print " Removes protection from Mobipocket books "
print " Removes protection from Mobipocket books "
print " Usage: "
print " Usage: "
print " %s <infile> <outfile> <PID>" % sys . argv [ 0 ]
print " %s <infile> <outfile> [ <Comma separated list of PIDs to try >] " % sys . argv [ 0 ]
return 1
return 1
else :
else :
infile = argv [ 1 ]
infile = argv [ 1 ]
outfile = argv [ 2 ]
outfile = argv [ 2 ]
pid = argv [ 3 ]
if len ( argv ) is 4 :
pidlist = argv [ 3 ] . split ( ' , ' )
else :
pidlist = { }
try :
try :
stripped_file = getUnencryptedBook ( infile , pid )
stripped_file = getUnencryptedBook WithList ( infile , pid list )
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