tools v3.0
First combined mobi/topaz kindle tool
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__version__ = '1.2'
|
||||
__version__ = '1.4'
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -44,344 +44,60 @@ import os, csv, getopt
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack, unpack, unpack_from
|
||||
|
||||
|
||||
#Exception Handling
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
#
|
||||
# crypto digestroutines
|
||||
#
|
||||
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
# determine if we are running as a calibre plugin
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
#
|
||||
# start of Kindle specific routines
|
||||
#
|
||||
|
||||
if not inCalibre:
|
||||
import mobidedrm
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
|
||||
global kindleDatabase
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += map[Q]
|
||||
result += map[R]
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
return result
|
||||
|
||||
|
||||
# Parse the Kindle.info file and return the records as a list of key-values
|
||||
def parseKindleInfo(kInfoFile):
|
||||
DB = {}
|
||||
infoReader = openKindleInfo(kInfoFile)
|
||||
infoReader.read(1)
|
||||
data = infoReader.read()
|
||||
if sys.platform.startswith('win'):
|
||||
items = data.split('{')
|
||||
else :
|
||||
items = data.split('[')
|
||||
for item in items:
|
||||
splito = item.split(':')
|
||||
DB[splito[0]] =splito[1]
|
||||
return DB
|
||||
|
||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
|
||||
def getKindleInfoValueForHash(hashedKey):
|
||||
global kindleDatabase
|
||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
||||
if sys.platform.startswith('win'):
|
||||
return CryptUnprotectData(encryptedValue,"")
|
||||
else:
|
||||
cleartext = CryptUnprotectData(encryptedValue)
|
||||
return decode(cleartext, charMap1)
|
||||
|
||||
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
|
||||
def getKindleInfoValueForKey(key):
|
||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
||||
|
||||
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
|
||||
def findNameForHash(hash):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
result = ""
|
||||
for name in names:
|
||||
if hash == encodeHash(name, charMap2):
|
||||
result = name
|
||||
break
|
||||
return result
|
||||
|
||||
# Print all the records from the kindle.info file (option -i)
|
||||
def printKindleInfo():
|
||||
for record in kindleDatabase:
|
||||
name = findNameForHash(record)
|
||||
if name != "" :
|
||||
print (name)
|
||||
print ("--------------------------")
|
||||
else :
|
||||
print ("Unknown Record")
|
||||
print getKindleInfoValueForHash(record)
|
||||
print "\n"
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
|
||||
# Returns two bit at offset from a bit field
|
||||
def getTwoBitsFromBitField(bitField,offset):
|
||||
byteNumber = offset // 4
|
||||
bitPosition = 6 - 2*(offset % 4)
|
||||
return ord(bitField[byteNumber]) >> bitPosition & 3
|
||||
|
||||
# Returns the six bits at offset from a bit field
|
||||
def getSixBitsFromBitField(bitField,offset):
|
||||
offset *= 3
|
||||
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
|
||||
return value
|
||||
|
||||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
|
||||
# Encryption table used to generate the device PID
|
||||
def generatePidEncryptionTable() :
|
||||
table = []
|
||||
for counter1 in range (0,0x100):
|
||||
value = counter1
|
||||
for counter2 in range (0,8):
|
||||
if (value & 1 == 0) :
|
||||
value = value >> 1
|
||||
else :
|
||||
value = value >> 1
|
||||
value = value ^ 0xEDB88320
|
||||
table.append(value)
|
||||
return table
|
||||
|
||||
# Seed value used to generate the device PID
|
||||
def generatePidSeed(table,dsn) :
|
||||
value = 0
|
||||
for counter in range (0,4) :
|
||||
index = (ord(dsn[counter]) ^ value) &0xFF
|
||||
value = (value >> 8) ^ table[index]
|
||||
return value
|
||||
|
||||
# Generate the device PID
|
||||
def generateDevicePID(table,dsn,nbRoll):
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
pid[index] = pid[index] ^ ord(dsn[counter])
|
||||
index = (index+1) %8
|
||||
for counter in range (0,8):
|
||||
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
||||
pidAscii += charMap4[index]
|
||||
return pidAscii
|
||||
|
||||
# convert from 8 digit PID to 10 digit PID with checksum
|
||||
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
|
||||
|
||||
|
||||
class MobiPeek:
|
||||
def loadSection(self, section):
|
||||
before, after = self.sections[section:section+2]
|
||||
self.f.seek(before)
|
||||
return self.f.read(after - before)
|
||||
def __init__(self, filename):
|
||||
self.f = file(filename, 'rb')
|
||||
self.header = self.f.read(78)
|
||||
self.ident = self.header[0x3C:0x3C+8]
|
||||
if self.ident != 'BOOKMOBI' and self.ident != 'TEXtREAd':
|
||||
raise DrmException('invalid file format')
|
||||
self.num_sections, = unpack_from('>H', self.header, 76)
|
||||
sections = self.f.read(self.num_sections*8)
|
||||
self.sections = unpack_from('>%dL' % (self.num_sections*2), sections, 0)[::2] + (0xfffffff, )
|
||||
self.sect0 = self.loadSection(0)
|
||||
self.f.close()
|
||||
def getBookTitle(self):
|
||||
# get book title
|
||||
toff, tlen = unpack('>II', self.sect0[0x54:0x5c])
|
||||
tend = toff + tlen
|
||||
title = self.sect0[toff:tend]
|
||||
return title
|
||||
def getexthData(self):
|
||||
# if exth region exists then grab it
|
||||
# get length of this header
|
||||
length, type, codepage, unique_id, version = unpack('>LLLLL', self.sect0[20:40])
|
||||
exth_flag, = unpack('>L', self.sect0[0x80:0x84])
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect0[16 + length:]
|
||||
return exth
|
||||
def isNotEncrypted(self):
|
||||
lock_type, = unpack('>H', self.sect0[0xC:0xC+2])
|
||||
if lock_type == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo
|
||||
# file to calculate the book pid.
|
||||
def getK4Pids(exth, title, kInfoFile=None):
|
||||
global kindleDatabase
|
||||
try:
|
||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
||||
except Exception, message:
|
||||
print(message)
|
||||
|
||||
if kindleDatabase != None :
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
||||
|
||||
# Get the HDD serial
|
||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
||||
|
||||
# Get the current user name
|
||||
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
||||
|
||||
print("\nDSN: " + DSN)
|
||||
|
||||
# Compute the device PID (for which I can tell, is used for nothing).
|
||||
# But hey, stuff being printed out is apparently cool.
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
|
||||
print("Device PID: " + checksumPid(devicePID))
|
||||
|
||||
# Compute book PID
|
||||
exth_records = {}
|
||||
nitems, = unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
|
||||
exth_records[209] = None
|
||||
# Parse the exth records, storing data indexed by type
|
||||
for i in xrange(nitems):
|
||||
type, size = unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
|
||||
exth_records[type] = content
|
||||
pos += size
|
||||
|
||||
# Grab the contents of the type 209 exth record
|
||||
if exth_records[209] != None:
|
||||
data = exth_records[209]
|
||||
else:
|
||||
raise DrmException("\nNo EXTH record type 209 - Perhaps not a K4 file?")
|
||||
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if exth_records[ord(data[i])] != None:
|
||||
token = exth_records[ord(data[i])]
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
||||
|
||||
print("Account Token: " + kindleAccountToken)
|
||||
|
||||
pidHash = SHA1(DSN+kindleAccountToken+exth_records[209]+token)
|
||||
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
|
||||
if exth_records[503] != None:
|
||||
print "Pid for " + exth_records[503] + ": " + bookPID
|
||||
else:
|
||||
print "Pid for " + title + ":" + bookPID
|
||||
return bookPID
|
||||
|
||||
raise DrmException("\nCould not access K4 data - Perhaps K4 is not installed/configured?")
|
||||
return null
|
||||
def zipUpDir(myzip, tempdir,localname):
|
||||
currentdir = tempdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tempdir, localfilePath)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC, K4M, and Mobi ebooks"
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] <infile> <outfile> " % progname
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
global kindleDatabase
|
||||
import mobidedrm
|
||||
|
||||
import topazextract
|
||||
import kgenpids
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
pidnums = ""
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
|
||||
print ' '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:")
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
@@ -394,126 +110,145 @@ def main(argv=sys.argv):
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pidnums = a
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
|
||||
kindleDatabase = None
|
||||
infile = args[0]
|
||||
outfile = args[1]
|
||||
DecodeErrorString = ""
|
||||
try:
|
||||
# first try with K4PC/K4M
|
||||
ex = MobiPeek(infile)
|
||||
if ex.isNotEncrypted():
|
||||
print "File was Not Encrypted"
|
||||
return 2
|
||||
title = ex.getBookTitle()
|
||||
exth = ex.getexthData()
|
||||
if exth=='':
|
||||
raise DrmException("Not a Kindle Mobipocket file")
|
||||
pid = getK4Pids(exth, title)
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
|
||||
except DrmException, e:
|
||||
DecodeErrorString += "Error trying default K4 info: " + str(e) + "\n"
|
||||
pass
|
||||
except mobidedrm.DrmException, e:
|
||||
DecodeErrorString += "Error trying default K4 info: " + str(e) + "\n"
|
||||
pass
|
||||
outdir = args[1]
|
||||
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
|
||||
# now try alternate kindle.info files
|
||||
if kInfoFiles:
|
||||
for infoFile in kInfoFiles:
|
||||
kindleDatabase = None
|
||||
try:
|
||||
title = ex.getBookTitle()
|
||||
exth = ex.getexthData()
|
||||
if exth=='':
|
||||
raise DrmException("Not a Kindle Mobipocket file")
|
||||
pid = getK4Pids(exth, title, infoFile)
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
|
||||
except DrmException, e:
|
||||
DecodeErrorString += "Error trying " + infoFile + " K4 info: " + str(e) + "\n"
|
||||
pass
|
||||
except mobidedrm.DrmException, e:
|
||||
DecodeErrorString += "Error trying " + infoFile + " K4 info: " + str(e) + "\n"
|
||||
pass
|
||||
else:
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
|
||||
# Lastly, try from the pid list
|
||||
pids = pidnums.split(',')
|
||||
for pid in pids:
|
||||
try:
|
||||
print 'Trying: "'+ pid + '"'
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
|
||||
except mobidedrm.DrmException:
|
||||
pass
|
||||
tempdir = tempfile.mkdtemp()
|
||||
mb = topazextract.TopazBook(infile, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
mb.processBook(pidlst)
|
||||
|
||||
# we could not unencrypt book
|
||||
print DecodeErrorString
|
||||
print "Error: Could Not Unencrypt Book"
|
||||
return 1
|
||||
except mobidedrm.DrmException, e:
|
||||
print " ... not suceessful " + str(e) + "\n"
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print str(e)
|
||||
print " Creating DeBug Full Zip Archive of Book"
|
||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
zipUpDir(myzip, tempdir, '')
|
||||
myzip.close()
|
||||
return 1
|
||||
|
||||
if mobi:
|
||||
outfile = os.path.join(outdir,bookname + '_nodrm' + '.azw')
|
||||
file(outfile, 'wb').write(unlocked_file)
|
||||
return 0
|
||||
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip1, tempdir, 'img')
|
||||
myzip1.close()
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(myzip2, tempdir, 'svg')
|
||||
zipUpDir(myzip2, tempdir, 'img')
|
||||
myzip2.close()
|
||||
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(tempdir,'xml')
|
||||
zipUpDir(myzip3, targetdir, '')
|
||||
zipUpDir(myzip3, tempdir, 'img')
|
||||
myzip3.close()
|
||||
|
||||
shutil.rmtree(tempdir)
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
if not __name__ == "__main__" and inCalibre:
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class K4DeDRM(FileTypePlugin):
|
||||
name = 'K4PC, K4Mac, Mobi DeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from K4PC, K4Mac, and Mobi files. \
|
||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||
version = (0, 1, 4) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
|
||||
version = (0, 1, 7) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
priority = 200 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
|
||||
# Head Topaz files off at the pass and warn the user that they will NOT
|
||||
# be decrypted. Changes the file extension from .azw or .prc to .tpz so
|
||||
# Calibre can at least read the metadata properly and the user can find
|
||||
# them by sorting on 'format'.
|
||||
with open(path_to_ebook, 'rb') as f:
|
||||
raw = f.read()
|
||||
if raw.startswith('TPZ'):
|
||||
tf = self.temporary_file('.tpz')
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "%s is a Topaz book. It will NOT be decrypted!" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
tf.write(raw)
|
||||
tf.close
|
||||
return tf.name
|
||||
|
||||
global kindleDatabase
|
||||
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
if sys.platform.startswith('win'):
|
||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
if sys.platform.startswith('darwin'):
|
||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
import kgenpids
|
||||
import zlib
|
||||
import zipfile
|
||||
import topazextract
|
||||
import mobidedrm
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
pidnums = self.site_customization
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
k4 = True
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
|
||||
# Get supplied list of PIDs to try from plugin customization.
|
||||
customvalues = self.site_customization.split(',')
|
||||
for customvalue in customvalues:
|
||||
customvalue = str(customvalue)
|
||||
customvalue = customvalue.strip()
|
||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||
pids.append(customvalue)
|
||||
else :
|
||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||
serials.append(customvalue)
|
||||
else:
|
||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||
|
||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
@@ -531,70 +266,68 @@ if not __name__ == "__main__" and inCalibre:
|
||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
||||
pass
|
||||
|
||||
# first try with book specifc pid from K4PC or K4M
|
||||
try:
|
||||
kindleDatabase = None
|
||||
ex = MobiPeek(path_to_ebook)
|
||||
if ex.isNotEncrypted():
|
||||
return path_to_ebook
|
||||
title = ex.getBookTitle()
|
||||
exth = ex.getexthData()
|
||||
if exth=='':
|
||||
raise DrmException("Not a Kindle Mobipocket file")
|
||||
pid = getK4Pids(exth, title)
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook,pid)
|
||||
except DrmException:
|
||||
pass
|
||||
except mobidedrm.DrmException:
|
||||
pass
|
||||
|
||||
mobi = True
|
||||
magic3 = file(path_to_ebook,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
tempdir = PersistentTemporaryDirectory()
|
||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||
|
||||
try:
|
||||
if mobi:
|
||||
unlocked_file = mb.processBook(pidlst)
|
||||
else:
|
||||
mb.processBook(pidlst)
|
||||
|
||||
except mobidedrm.DrmException:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
except topazextract.TpzDRMError:
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
|
||||
print "Success!"
|
||||
if mobi:
|
||||
of = self.temporary_file(bookname+'.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# Now try alternate kindle info files
|
||||
if kInfoFiles:
|
||||
for infoFile in kInfoFiles:
|
||||
kindleDatabase = None
|
||||
try:
|
||||
title = ex.getBookTitle()
|
||||
exth = ex.getexthData()
|
||||
if exth=='':
|
||||
raise DrmException("Not a Kindle Mobipocket file")
|
||||
pid = getK4Pids(exth, title, infoFile)
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook,pid)
|
||||
except DrmException:
|
||||
pass
|
||||
except mobidedrm.DrmException:
|
||||
pass
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
# now try from the pid list
|
||||
pids = pidnums.split(',')
|
||||
for pid in pids:
|
||||
try:
|
||||
unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook, pid)
|
||||
except mobidedrm.DrmException:
|
||||
pass
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
|
||||
#if you reached here then no luck raise and exception
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||
return ""
|
||||
# topaz: build up zip archives of results
|
||||
print " Creating HTML ZIP Archive"
|
||||
of = self.temporary_file(bookname + '.zip')
|
||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
||||
zipUpDir(myzip, tempdir, 'img')
|
||||
myzip.close()
|
||||
return of.name
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter each 10 character PID separated by a comma (no spaces).'
|
||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
||||
|
||||
@@ -1,159 +1,14 @@
|
||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
#Exception Handling
|
||||
class K4MDrmException(Exception):
|
||||
pass
|
||||
|
||||
import signal
|
||||
import threading
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
# **heavily** chopped up and modfied version of asyncproc.py
|
||||
# to make it actually work on Windows as well as Mac/Linux
|
||||
# For the original see:
|
||||
# "http://www.lysator.liu.se/~bellman/download/"
|
||||
# author is "Thomas Bellman <bellman@lysator.liu.se>"
|
||||
# available under GPL version 3 or Later
|
||||
|
||||
# create an asynchronous subprocess whose output can be collected in
|
||||
# a non-blocking manner
|
||||
|
||||
# What a mess! Have to use threads just to get non-blocking io
|
||||
# in a cross-platform manner
|
||||
|
||||
# luckily all thread use is hidden within this class
|
||||
|
||||
class Process(object):
|
||||
def __init__(self, *params, **kwparams):
|
||||
if len(params) <= 3:
|
||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
||||
if len(params) <= 4:
|
||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
||||
if len(params) <= 5:
|
||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
||||
self.__pending_input = []
|
||||
self.__collected_outdata = []
|
||||
self.__collected_errdata = []
|
||||
self.__exitstatus = None
|
||||
self.__lock = threading.Lock()
|
||||
self.__inputsem = threading.Semaphore(0)
|
||||
self.__quit = False
|
||||
|
||||
self.__process = subprocess.Popen(*params, **kwparams)
|
||||
|
||||
if self.__process.stdin:
|
||||
self.__stdin_thread = threading.Thread(
|
||||
name="stdin-thread",
|
||||
target=self.__feeder, args=(self.__pending_input,
|
||||
self.__process.stdin))
|
||||
self.__stdin_thread.setDaemon(True)
|
||||
self.__stdin_thread.start()
|
||||
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
def pid(self):
|
||||
return self.__process.pid
|
||||
|
||||
def kill(self, signal):
|
||||
self.__process.send_signal(signal)
|
||||
|
||||
# check on subprocess (pass in 'nowait') to act like poll
|
||||
def wait(self, flag):
|
||||
if flag.lower() == 'nowait':
|
||||
rc = self.__process.poll()
|
||||
else:
|
||||
rc = self.__process.wait()
|
||||
if rc != None:
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread.join()
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread.join()
|
||||
return self.__process.returncode
|
||||
|
||||
def terminate(self):
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
self.__process.terminate()
|
||||
|
||||
# thread gets data from subprocess stdout
|
||||
def __reader(self, collector, source):
|
||||
while True:
|
||||
data = os.read(source.fileno(), 65536)
|
||||
self.__lock.acquire()
|
||||
collector.append(data)
|
||||
self.__lock.release()
|
||||
if data == "":
|
||||
source.close()
|
||||
break
|
||||
return
|
||||
|
||||
# thread feeds data to subprocess stdin
|
||||
def __feeder(self, pending, drain):
|
||||
while True:
|
||||
self.__inputsem.acquire()
|
||||
self.__lock.acquire()
|
||||
if not pending and self.__quit:
|
||||
drain.close()
|
||||
self.__lock.release()
|
||||
break
|
||||
data = pending.pop(0)
|
||||
self.__lock.release()
|
||||
drain.write(data)
|
||||
|
||||
# non-blocking read of data from subprocess stdout
|
||||
def read(self):
|
||||
self.__lock.acquire()
|
||||
outdata = "".join(self.__collected_outdata)
|
||||
del self.__collected_outdata[:]
|
||||
self.__lock.release()
|
||||
return outdata
|
||||
|
||||
# non-blocking read of data from subprocess stderr
|
||||
def readerr(self):
|
||||
self.__lock.acquire()
|
||||
errdata = "".join(self.__collected_errdata)
|
||||
del self.__collected_errdata[:]
|
||||
self.__lock.release()
|
||||
return errdata
|
||||
|
||||
# non-blocking write to stdin of subprocess
|
||||
def write(self, data):
|
||||
if self.__process.stdin is None:
|
||||
raise ValueError("Writing to process with stdin not a pipe")
|
||||
self.__lock.acquire()
|
||||
self.__pending_input.append(data)
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
# close stdinput of subprocess
|
||||
def closeinput(self):
|
||||
self.__lock.acquire()
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
|
||||
# interface to needed routines in openssl's libcrypto
|
||||
def _load_crypto_libcrypto():
|
||||
@@ -236,6 +91,15 @@ LibCrypto = _load_crypto()
|
||||
# Utility Routines
|
||||
#
|
||||
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
def GetVolumeSerialNumber():
|
||||
@@ -244,10 +108,9 @@ def GetVolumeSerialNumber():
|
||||
return sernum
|
||||
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
poll = p.wait('wait')
|
||||
results = p.read()
|
||||
reslst = results.split('\n')
|
||||
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p.communicate()
|
||||
reslst = out1.split('\n')
|
||||
cnt = len(reslst)
|
||||
bsdname = None
|
||||
sernum = None
|
||||
@@ -274,11 +137,6 @@ def GetUserName():
|
||||
username = os.getenv('USER')
|
||||
return username
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
@@ -309,16 +167,16 @@ def CryptUnprotectData(encryptedData):
|
||||
cleartext = crp.decrypt(encryptedData)
|
||||
return cleartext
|
||||
|
||||
|
||||
# Locate and open the .kindle-info file
|
||||
def openKindleInfo(kInfoFile=None):
|
||||
if kInfoFile == None:
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p1 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
poll = p1.wait('wait')
|
||||
results = p1.read()
|
||||
reslst = results.split('\n')
|
||||
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p1.communicate()
|
||||
reslst = out1.split('\n')
|
||||
kinfopath = 'NONE'
|
||||
cnt = len(reslst)
|
||||
for j in xrange(cnt):
|
||||
|
||||
@@ -19,17 +19,12 @@ advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
|
||||
#
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
#
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
#
|
||||
# Exceptions for all the problems that might happen during the script
|
||||
#
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
# 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
|
||||
# files reveals that a confusion 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
|
||||
@@ -39,13 +39,13 @@
|
||||
# Removed the disabled Calibre plug-in code
|
||||
# Permit use of 8-digit PIDs
|
||||
# 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
|
||||
|
||||
__version__ = '0.20'
|
||||
__version__ = '0.22'
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
@@ -55,10 +55,19 @@ class Unbuffered:
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# MobiBook Utility Routines
|
||||
#
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
@@ -70,7 +79,6 @@ def PC1(key, src, decryption=True):
|
||||
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;
|
||||
@@ -131,7 +139,9 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
class DrmStripper:
|
||||
|
||||
|
||||
class MobiBook:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
@@ -140,6 +150,78 @@ class DrmStripper:
|
||||
off = self.sections[section][0]
|
||||
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':
|
||||
raise DrmException("invalid file format")
|
||||
|
||||
# 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])
|
||||
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 = {}
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
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
|
||||
pos += size
|
||||
|
||||
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 = None
|
||||
token = None
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# Parse the 209 data to find the the exth record with the token data.
|
||||
# The last character of the 209 data points to the record with the token.
|
||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
||||
for i in xrange(len(data)):
|
||||
if ord(data[i]) != 0:
|
||||
if self.meta_array[ord(data[i])] != None:
|
||||
token = self.meta_array[ord(data[i])]
|
||||
return rec209, token
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
@@ -152,134 +234,122 @@ class DrmStripper:
|
||||
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
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
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
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, 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])
|
||||
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
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
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
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver:
|
||||
found_key = finalkey
|
||||
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])
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
return self.data_file
|
||||
if crypto_type == 1:
|
||||
raise DrmException("Cannot decode Mobipocket encryption type 1")
|
||||
if crypto_type != 2:
|
||||
raise DrmException("Cannot decode unknown Mobipocket 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)
|
||||
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
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)
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
|
||||
# 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)
|
||||
|
||||
# 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 . . .",
|
||||
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"
|
||||
|
||||
def getResult(self):
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
new_data = self.data_file[:self.sections[1][0]]
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.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:]
|
||||
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
|
||||
|
||||
def getUnencryptedBook(infile,pid):
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
data_file = file(infile, 'rb').read()
|
||||
strippedFile = DrmStripper(data_file, pid)
|
||||
return strippedFile.getResult()
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile)
|
||||
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):
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(argv)<4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
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
|
||||
else:
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
pid = argv[3]
|
||||
pidlist = argv[3].split(',')
|
||||
try:
|
||||
stripped_file = getUnencryptedBook(infile, pid)
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
|
||||
Reference in New Issue
Block a user