505 lines
13 KiB
Python
505 lines
13 KiB
Python
|
#! /usr/bin/python
|
||
|
|
||
|
from __future__ import with_statement
|
||
|
|
||
|
import csv
|
||
|
import sys
|
||
|
import os
|
||
|
import getopt
|
||
|
import zlib
|
||
|
from struct import pack
|
||
|
from struct import unpack
|
||
|
|
||
|
MAX_PATH = 255
|
||
|
|
||
|
# Put the first 8 characters of your Kindle PID here
|
||
|
# or supply it with the -p option in the command line
|
||
|
####################################################
|
||
|
kindlePID = "12345678"
|
||
|
####################################################
|
||
|
|
||
|
global bookFile
|
||
|
global bookPayloadOffset
|
||
|
global bookHeaderRecords
|
||
|
global bookMetadata
|
||
|
global bookKey
|
||
|
global command
|
||
|
|
||
|
#
|
||
|
# Exceptions for all the problems that might happen during the script
|
||
|
#
|
||
|
|
||
|
class CMBDTCError(Exception):
|
||
|
pass
|
||
|
|
||
|
class CMBDTCFatal(Exception):
|
||
|
pass
|
||
|
|
||
|
|
||
|
#
|
||
|
# Open the book file at path
|
||
|
#
|
||
|
|
||
|
def openBook(path):
|
||
|
try:
|
||
|
return open(path,'rb')
|
||
|
except:
|
||
|
raise CMBDTCFatal("Could not open book file: " + path)
|
||
|
|
||
|
#
|
||
|
# Get a 7 bit encoded number from the book file
|
||
|
#
|
||
|
|
||
|
def bookReadEncodedNumber():
|
||
|
flag = False
|
||
|
data = ord(bookFile.read(1))
|
||
|
|
||
|
if data == 0xFF:
|
||
|
flag = True
|
||
|
data = ord(bookFile.read(1))
|
||
|
|
||
|
if data >= 0x80:
|
||
|
datax = (data & 0x7F)
|
||
|
while data >= 0x80 :
|
||
|
data = ord(bookFile.read(1))
|
||
|
datax = (datax <<7) + (data & 0x7F)
|
||
|
data = datax
|
||
|
|
||
|
if flag:
|
||
|
data = -data
|
||
|
return data
|
||
|
|
||
|
#
|
||
|
# Encode a number in 7 bit format
|
||
|
#
|
||
|
|
||
|
def encodeNumber(number):
|
||
|
result = ""
|
||
|
negative = False
|
||
|
flag = 0
|
||
|
print("Using encodeNumber routine")
|
||
|
|
||
|
if number < 0 :
|
||
|
number = -number + 1
|
||
|
negative = True
|
||
|
|
||
|
while True:
|
||
|
byte = number & 0x7F
|
||
|
number = number >> 7
|
||
|
byte += flag
|
||
|
result += chr(byte)
|
||
|
flag = 0x80
|
||
|
if number == 0 : break
|
||
|
|
||
|
if negative:
|
||
|
result += chr(0xFF)
|
||
|
|
||
|
return result[::-1]
|
||
|
|
||
|
#
|
||
|
# Get a length prefixed string from the file
|
||
|
#
|
||
|
|
||
|
def bookReadString():
|
||
|
stringLength = bookReadEncodedNumber()
|
||
|
return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0]
|
||
|
|
||
|
#
|
||
|
# Returns a length prefixed string
|
||
|
#
|
||
|
|
||
|
def lengthPrefixString(data):
|
||
|
return encodeNumber(len(data))+data
|
||
|
|
||
|
|
||
|
#
|
||
|
# Read and return the data of one header record at the current book file position [[offset,decompressedLength,compressedLength],...]
|
||
|
#
|
||
|
|
||
|
def bookReadHeaderRecordData():
|
||
|
nbValues = bookReadEncodedNumber()
|
||
|
values = []
|
||
|
for i in range (0,nbValues):
|
||
|
values.append([bookReadEncodedNumber(),bookReadEncodedNumber(),bookReadEncodedNumber()])
|
||
|
return values
|
||
|
|
||
|
#
|
||
|
# Read and parse one header record at the current book file position and return the associated data [[offset,decompressedLength,compressedLength],...]
|
||
|
#
|
||
|
|
||
|
def parseTopazHeaderRecord():
|
||
|
if ord(bookFile.read(1)) != 0x63:
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Header")
|
||
|
|
||
|
tag = bookReadString()
|
||
|
record = bookReadHeaderRecordData()
|
||
|
return [tag,record]
|
||
|
|
||
|
#
|
||
|
# Parse the header of a Topaz file, get all the header records and the offset for the payload
|
||
|
#
|
||
|
|
||
|
def parseTopazHeader():
|
||
|
global bookHeaderRecords
|
||
|
global bookPayloadOffset
|
||
|
magic = unpack("4s",bookFile.read(4))[0]
|
||
|
|
||
|
if magic != 'TPZ0':
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Header, not a Topaz file")
|
||
|
|
||
|
nbRecords = bookReadEncodedNumber()
|
||
|
bookHeaderRecords = {}
|
||
|
|
||
|
for i in range (0,nbRecords):
|
||
|
result = parseTopazHeaderRecord()
|
||
|
print result[0], result[1]
|
||
|
bookHeaderRecords[result[0]] = result[1]
|
||
|
|
||
|
if ord(bookFile.read(1)) != 0x64 :
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Header")
|
||
|
|
||
|
bookPayloadOffset = bookFile.tell()
|
||
|
|
||
|
#
|
||
|
# Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed
|
||
|
# Correction, the record is correctly decompressed too
|
||
|
#
|
||
|
|
||
|
def getBookPayloadRecord(name, index):
|
||
|
encrypted = False
|
||
|
compressed = False
|
||
|
|
||
|
try:
|
||
|
recordOffset = bookHeaderRecords[name][index][0]
|
||
|
except:
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Record, record not found")
|
||
|
|
||
|
bookFile.seek(bookPayloadOffset + recordOffset)
|
||
|
|
||
|
tag = bookReadString()
|
||
|
if tag != name :
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Record, record name doesn't match")
|
||
|
|
||
|
recordIndex = bookReadEncodedNumber()
|
||
|
|
||
|
if recordIndex < 0 :
|
||
|
encrypted = True
|
||
|
recordIndex = -recordIndex -1
|
||
|
|
||
|
if recordIndex != index :
|
||
|
raise CMBDTCFatal("Parse Error : Invalid Record, index doesn't match")
|
||
|
|
||
|
if (bookHeaderRecords[name][index][2] > 0):
|
||
|
compressed = True
|
||
|
record = bookFile.read(bookHeaderRecords[name][index][2])
|
||
|
else:
|
||
|
record = bookFile.read(bookHeaderRecords[name][index][1])
|
||
|
|
||
|
if encrypted:
|
||
|
ctx = topazCryptoInit(bookKey)
|
||
|
record = topazCryptoDecrypt(record,ctx)
|
||
|
|
||
|
if compressed:
|
||
|
record = zlib.decompress(record)
|
||
|
|
||
|
return record
|
||
|
|
||
|
#
|
||
|
# Extract, decrypt and decompress a book record indicated by name and index and print it or save it in "filename"
|
||
|
#
|
||
|
|
||
|
def extractBookPayloadRecord(name, index, filename):
|
||
|
compressed = False
|
||
|
|
||
|
try:
|
||
|
compressed = bookHeaderRecords[name][index][2] != 0
|
||
|
record = getBookPayloadRecord(name,index)
|
||
|
except:
|
||
|
print("Could not find record")
|
||
|
|
||
|
# if compressed:
|
||
|
# try:
|
||
|
# record = zlib.decompress(record)
|
||
|
# except:
|
||
|
# raise CMBDTCFatal("Could not decompress record")
|
||
|
|
||
|
if filename != "":
|
||
|
try:
|
||
|
file = open(filename,"wb")
|
||
|
file.write(record)
|
||
|
file.close()
|
||
|
except:
|
||
|
raise CMBDTCFatal("Could not write to destination file")
|
||
|
else:
|
||
|
print(record)
|
||
|
|
||
|
#
|
||
|
# return next record [key,value] from the book metadata from the current book position
|
||
|
#
|
||
|
|
||
|
def readMetadataRecord():
|
||
|
return [bookReadString(),bookReadString()]
|
||
|
|
||
|
#
|
||
|
# Parse the metadata record from the book payload and return a list of [key,values]
|
||
|
#
|
||
|
|
||
|
def parseMetadata():
|
||
|
global bookHeaderRecords
|
||
|
global bookPayloadAddress
|
||
|
global bookMetadata
|
||
|
bookMetadata = {}
|
||
|
bookFile.seek(bookPayloadOffset + bookHeaderRecords["metadata"][0][0])
|
||
|
tag = bookReadString()
|
||
|
if tag != "metadata" :
|
||
|
raise CMBDTCFatal("Parse Error : Record Names Don't Match")
|
||
|
|
||
|
flags = ord(bookFile.read(1))
|
||
|
nbRecords = ord(bookFile.read(1))
|
||
|
|
||
|
for i in range (0,nbRecords) :
|
||
|
record =readMetadataRecord()
|
||
|
bookMetadata[record[0]] = record[1]
|
||
|
|
||
|
#
|
||
|
# Context initialisation for the Topaz Crypto
|
||
|
#
|
||
|
|
||
|
def topazCryptoInit(key):
|
||
|
ctx1 = 0x0CAFFE19E
|
||
|
|
||
|
for keyChar in key:
|
||
|
keyByte = ord(keyChar)
|
||
|
ctx2 = ctx1
|
||
|
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||
|
return [ctx1,ctx2]
|
||
|
|
||
|
#
|
||
|
# decrypt data with the context prepared by topazCryptoInit()
|
||
|
#
|
||
|
|
||
|
def topazCryptoDecrypt(data, ctx):
|
||
|
ctx1 = ctx[0]
|
||
|
ctx2 = ctx[1]
|
||
|
|
||
|
plainText = ""
|
||
|
|
||
|
for dataChar in data:
|
||
|
dataByte = ord(dataChar)
|
||
|
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||
|
ctx2 = ctx1
|
||
|
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||
|
plainText += chr(m)
|
||
|
|
||
|
return plainText
|
||
|
|
||
|
#
|
||
|
# Decrypt a payload record with the PID
|
||
|
#
|
||
|
|
||
|
def decryptRecord(data,PID):
|
||
|
ctx = topazCryptoInit(PID)
|
||
|
return topazCryptoDecrypt(data, ctx)
|
||
|
|
||
|
#
|
||
|
# Try to decrypt a dkey record (contains the book PID)
|
||
|
#
|
||
|
|
||
|
def decryptDkeyRecord(data,PID):
|
||
|
record = decryptRecord(data,PID)
|
||
|
fields = unpack("3sB8sB8s3s",record)
|
||
|
|
||
|
if fields[0] != "PID" or fields[5] != "pid" :
|
||
|
raise CMBDTCError("Didn't find PID magic numbers in record")
|
||
|
elif fields[1] != 8 or fields[3] != 8 :
|
||
|
raise CMBDTCError("Record didn't contain correct length fields")
|
||
|
elif fields[2] != PID :
|
||
|
raise CMBDTCError("Record didn't contain PID")
|
||
|
|
||
|
return fields[4]
|
||
|
|
||
|
#
|
||
|
# Decrypt all the book's dkey records (contain the book PID)
|
||
|
#
|
||
|
|
||
|
def decryptDkeyRecords(data,PID):
|
||
|
nbKeyRecords = ord(data[0])
|
||
|
records = []
|
||
|
data = data[1:]
|
||
|
for i in range (0,nbKeyRecords):
|
||
|
length = ord(data[0])
|
||
|
try:
|
||
|
key = decryptDkeyRecord(data[1:length+1],PID)
|
||
|
records.append(key)
|
||
|
except CMBDTCError:
|
||
|
pass
|
||
|
data = data[1+length:]
|
||
|
|
||
|
return records
|
||
|
|
||
|
#
|
||
|
# Create decrypted book payload
|
||
|
#
|
||
|
|
||
|
def createDecryptedPayload(payload):
|
||
|
for headerRecord in bookHeaderRecords:
|
||
|
name = headerRecord
|
||
|
if name != "dkey" :
|
||
|
ext = '.dat'
|
||
|
if name == 'img' : ext = '.jpg'
|
||
|
for index in range (0,len(bookHeaderRecords[name])) :
|
||
|
fnum = "%04d" % index
|
||
|
fname = name + fnum + ext
|
||
|
destdir = payload
|
||
|
if name == 'img':
|
||
|
destdir = os.path.join(payload,'img')
|
||
|
if name == 'page':
|
||
|
destdir = os.path.join(payload,'page')
|
||
|
if name == 'glyphs':
|
||
|
destdir = os.path.join(payload,'glyphs')
|
||
|
outputFile = os.path.join(destdir,fname)
|
||
|
file(outputFile, 'wb').write(getBookPayloadRecord(name, index))
|
||
|
|
||
|
|
||
|
# Create decrypted book
|
||
|
#
|
||
|
|
||
|
def createDecryptedBook(outdir):
|
||
|
if not os.path.exists(outdir):
|
||
|
os.makedirs(outdir)
|
||
|
|
||
|
destdir = os.path.join(outdir,'img')
|
||
|
if not os.path.exists(destdir):
|
||
|
os.makedirs(destdir)
|
||
|
|
||
|
destdir = os.path.join(outdir,'page')
|
||
|
if not os.path.exists(destdir):
|
||
|
os.makedirs(destdir)
|
||
|
|
||
|
destdir = os.path.join(outdir,'glyphs')
|
||
|
if not os.path.exists(destdir):
|
||
|
os.makedirs(destdir)
|
||
|
|
||
|
createDecryptedPayload(outdir)
|
||
|
|
||
|
|
||
|
#
|
||
|
# Set the command to execute by the programm according to cmdLine parameters
|
||
|
#
|
||
|
|
||
|
def setCommand(name) :
|
||
|
global command
|
||
|
if command != "" :
|
||
|
raise CMBDTCFatal("Invalid command line parameters")
|
||
|
else :
|
||
|
command = name
|
||
|
|
||
|
#
|
||
|
# Program usage
|
||
|
#
|
||
|
|
||
|
def usage():
|
||
|
print("\nUsage:")
|
||
|
print("\ncmbtc_dump_linux.py [options] bookFileName\n")
|
||
|
print("-p Adds a PID to the list of PIDs that are tried to decrypt the book key (can be used several times)")
|
||
|
print("-d Dumps the unencrypted book as files to outdir")
|
||
|
print("-o Output directory to save book files to")
|
||
|
print("-v Verbose (can be used several times)")
|
||
|
|
||
|
|
||
|
#
|
||
|
# Main
|
||
|
#
|
||
|
|
||
|
def main(argv=sys.argv):
|
||
|
global bookMetadata
|
||
|
global bookKey
|
||
|
global bookFile
|
||
|
global command
|
||
|
|
||
|
progname = os.path.basename(argv[0])
|
||
|
|
||
|
verbose = 0
|
||
|
recordName = ""
|
||
|
recordIndex = 0
|
||
|
outdir = ""
|
||
|
PIDs = []
|
||
|
command = ""
|
||
|
|
||
|
# Preloads your Kindle pid from the top of the program.
|
||
|
PIDs.append(kindlePID)
|
||
|
|
||
|
try:
|
||
|
opts, args = getopt.getopt(sys.argv[1:], "vo:p:d")
|
||
|
except getopt.GetoptError, err:
|
||
|
# print help information and exit:
|
||
|
print str(err) # will print something like "option -a not recognized"
|
||
|
usage()
|
||
|
sys.exit(2)
|
||
|
|
||
|
if len(opts) == 0 and len(args) == 0 :
|
||
|
usage()
|
||
|
sys.exit(2)
|
||
|
|
||
|
for o, a in opts:
|
||
|
if o == "-v":
|
||
|
verbose+=1
|
||
|
if o =="-o":
|
||
|
if a == None :
|
||
|
raise CMBDTCFatal("Invalid parameter for -o")
|
||
|
outdir = a
|
||
|
if o =="-p":
|
||
|
PIDs.append(a)
|
||
|
if o =="-d":
|
||
|
setCommand("doit")
|
||
|
|
||
|
if command == "" :
|
||
|
raise CMBDTCFatal("No action supplied on command line")
|
||
|
|
||
|
#
|
||
|
# Open book and parse metadata
|
||
|
#
|
||
|
|
||
|
if len(args) == 1:
|
||
|
|
||
|
bookFile = openBook(args[0])
|
||
|
parseTopazHeader()
|
||
|
parseMetadata()
|
||
|
|
||
|
#
|
||
|
# Decrypt book key
|
||
|
#
|
||
|
|
||
|
dkey = getBookPayloadRecord('dkey', 0)
|
||
|
|
||
|
bookKeys = []
|
||
|
for PID in PIDs :
|
||
|
bookKeys+=decryptDkeyRecords(dkey,PID)
|
||
|
|
||
|
if len(bookKeys) == 0 :
|
||
|
if verbose > 0 :
|
||
|
print ("Book key could not be found. Maybe this book is not registered with this device.")
|
||
|
else :
|
||
|
bookKey = bookKeys[0]
|
||
|
if verbose > 0:
|
||
|
print("Book key: " + bookKey.encode('hex'))
|
||
|
|
||
|
|
||
|
|
||
|
if command == "printRecord" :
|
||
|
extractBookPayloadRecord(recordName,int(recordIndex),outputFile)
|
||
|
if outputFile != "" and verbose>0 :
|
||
|
print("Wrote record to file: "+outputFile)
|
||
|
elif command == "doit" :
|
||
|
if outdir != "" :
|
||
|
createDecryptedBook(outdir)
|
||
|
if verbose >0 :
|
||
|
print ("Decrypted book saved. Don't pirate!")
|
||
|
elif verbose > 0:
|
||
|
print("Output directory name was not supplied.")
|
||
|
|
||
|
return 0
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
sys.exit(main())
|