mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2024-11-05 13:36:10 +06:00
a7856f5c32
First combined mobi/topaz kindle tool
901 lines
24 KiB
Python
901 lines
24 KiB
Python
#! /usr/bin/python
|
|
|
|
"""
|
|
|
|
Comprehensive Mazama Book DRM with Topaz Cryptography V2.2
|
|
|
|
-----BEGIN PUBLIC KEY-----
|
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
|
|
M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
|
|
B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
|
|
y2/pHuYme7U1TsgSjwIDAQAB
|
|
-----END PUBLIC KEY-----
|
|
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
|
|
import csv
|
|
import sys
|
|
import os
|
|
import getopt
|
|
import zlib
|
|
from struct import pack
|
|
from struct import unpack
|
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
|
string_at, Structure, c_void_p, cast
|
|
import _winreg as winreg
|
|
import Tkinter
|
|
import Tkconstants
|
|
import tkMessageBox
|
|
import traceback
|
|
import hashlib
|
|
|
|
MAX_PATH = 255
|
|
|
|
kernel32 = windll.kernel32
|
|
advapi32 = windll.advapi32
|
|
crypt32 = windll.crypt32
|
|
|
|
global kindleDatabase
|
|
global bookFile
|
|
global bookPayloadOffset
|
|
global bookHeaderRecords
|
|
global bookMetadata
|
|
global bookKey
|
|
global command
|
|
|
|
#
|
|
# 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 CMBDTCError(Exception):
|
|
pass
|
|
|
|
class CMBDTCFatal(Exception):
|
|
pass
|
|
|
|
#
|
|
# Stolen stuff
|
|
#
|
|
|
|
class DataBlob(Structure):
|
|
_fields_ = [('cbData', c_uint),
|
|
('pbData', c_void_p)]
|
|
DataBlob_p = POINTER(DataBlob)
|
|
|
|
def GetSystemDirectory():
|
|
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
|
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
|
GetSystemDirectoryW.restype = c_uint
|
|
def GetSystemDirectory():
|
|
buffer = create_unicode_buffer(MAX_PATH + 1)
|
|
GetSystemDirectoryW(buffer, len(buffer))
|
|
return buffer.value
|
|
return GetSystemDirectory
|
|
GetSystemDirectory = GetSystemDirectory()
|
|
|
|
|
|
def GetVolumeSerialNumber():
|
|
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
|
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
|
POINTER(c_uint), POINTER(c_uint),
|
|
POINTER(c_uint), c_wchar_p, c_uint]
|
|
GetVolumeInformationW.restype = c_uint
|
|
def GetVolumeSerialNumber(path):
|
|
vsn = c_uint(0)
|
|
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
|
return vsn.value
|
|
return GetVolumeSerialNumber
|
|
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
|
|
|
|
|
def GetUserName():
|
|
GetUserNameW = advapi32.GetUserNameW
|
|
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
|
GetUserNameW.restype = c_uint
|
|
def GetUserName():
|
|
buffer = create_unicode_buffer(32)
|
|
size = c_uint(len(buffer))
|
|
while not GetUserNameW(buffer, byref(size)):
|
|
buffer = create_unicode_buffer(len(buffer) * 2)
|
|
size.value = len(buffer)
|
|
return buffer.value.encode('utf-16-le')[::2]
|
|
return GetUserName
|
|
GetUserName = GetUserName()
|
|
|
|
|
|
def CryptUnprotectData():
|
|
_CryptUnprotectData = crypt32.CryptUnprotectData
|
|
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
|
c_void_p, c_void_p, c_uint, DataBlob_p]
|
|
_CryptUnprotectData.restype = c_uint
|
|
def CryptUnprotectData(indata, entropy):
|
|
indatab = create_string_buffer(indata)
|
|
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
|
entropyb = create_string_buffer(entropy)
|
|
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
|
outdata = DataBlob()
|
|
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
|
None, None, 0, byref(outdata)):
|
|
raise CMBDTCFatal("Failed to Unprotect Data")
|
|
return string_at(outdata.pbData, outdata.cbData)
|
|
return CryptUnprotectData
|
|
CryptUnprotectData = CryptUnprotectData()
|
|
|
|
#
|
|
# Returns the MD5 digest of "message"
|
|
#
|
|
|
|
def MD5(message):
|
|
ctx = hashlib.md5()
|
|
ctx.update(message)
|
|
return ctx.digest()
|
|
|
|
#
|
|
# Returns the MD5 digest of "message"
|
|
#
|
|
|
|
def SHA1(message):
|
|
ctx = hashlib.sha1()
|
|
ctx.update(message)
|
|
return ctx.digest()
|
|
|
|
#
|
|
# Open the book file at path
|
|
#
|
|
|
|
def openBook(path):
|
|
try:
|
|
return open(path,'rb')
|
|
except:
|
|
raise CMBDTCFatal("Could not open book file: " + path)
|
|
#
|
|
# 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),2):
|
|
high = map.find(data[i])
|
|
low = map.find(data[i+1])
|
|
value = (((high * 0x40) ^ 0x80) & 0xFF) + low
|
|
result += pack("B",value)
|
|
return result
|
|
|
|
#
|
|
# Locate and open the Kindle.info file (Hopefully in the way it is done in the Kindle application)
|
|
#
|
|
|
|
def openKindleInfo():
|
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
|
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
|
return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
|
|
|
|
#
|
|
# Parse the Kindle.info file and return the records as a list of key-values
|
|
#
|
|
|
|
def parseKindleInfo():
|
|
DB = {}
|
|
infoReader = openKindleInfo()
|
|
infoReader.read(1)
|
|
data = infoReader.read()
|
|
items = data.split('{')
|
|
|
|
for item in items:
|
|
splito = item.split(':')
|
|
DB[splito[0]] =splito[1]
|
|
return DB
|
|
|
|
#
|
|
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
|
|
#
|
|
|
|
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 name
|
|
|
|
#
|
|
# 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 ("--------------------------\n")
|
|
else :
|
|
print ("Unknown Record")
|
|
print getKindleInfoValueForHash(record)
|
|
print "\n"
|
|
#
|
|
# 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)
|
|
return CryptUnprotectData(encryptedValue,"")
|
|
|
|
#
|
|
# 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))
|
|
|
|
#
|
|
# 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
|
|
|
|
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 :
|
|
if (byte == 0xFF and negative == False) :
|
|
result += chr(0x80)
|
|
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,compressedLength,decompressedLength],...]
|
|
#
|
|
|
|
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,compressedLength,decompressedLength],...]
|
|
#
|
|
|
|
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()
|
|
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
|
|
#
|
|
|
|
def getBookPayloadRecord(name, index):
|
|
encrypted = 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 :
|
|
record = bookFile.read(bookHeaderRecords[name][index][2])
|
|
else:
|
|
record = bookFile.read(bookHeaderRecords[name][index][1])
|
|
|
|
if encrypted:
|
|
ctx = topazCryptoInit(bookKey)
|
|
record = topazCryptoDecrypt(record,ctx)
|
|
|
|
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]
|
|
|
|
#
|
|
# 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
|
|
|
|
#
|
|
# 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
|
|
|
|
#
|
|
# 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
|
|
|
|
#
|
|
# Create decrypted book payload
|
|
#
|
|
|
|
def createDecryptedPayload(payload):
|
|
|
|
# store data to be able to create the header later
|
|
headerData= []
|
|
currentOffset = 0
|
|
|
|
# Add social DRM to decrypted files
|
|
|
|
try:
|
|
data = getKindleInfoValueForKey("kindle.name.info")+":"+ getKindleInfoValueForKey("login")
|
|
if payload!= None:
|
|
payload.write(lengthPrefixString("sdrm"))
|
|
payload.write(encodeNumber(0))
|
|
payload.write(data)
|
|
else:
|
|
currentOffset += len(lengthPrefixString("sdrm"))
|
|
currentOffset += len(encodeNumber(0))
|
|
currentOffset += len(data)
|
|
except:
|
|
pass
|
|
|
|
for headerRecord in bookHeaderRecords:
|
|
name = headerRecord
|
|
newRecord = []
|
|
|
|
if name != "dkey" :
|
|
|
|
for index in range (0,len(bookHeaderRecords[name])) :
|
|
offset = currentOffset
|
|
|
|
if payload != None:
|
|
# write tag
|
|
payload.write(lengthPrefixString(name))
|
|
# write data
|
|
payload.write(encodeNumber(index))
|
|
payload.write(getBookPayloadRecord(name, index))
|
|
|
|
else :
|
|
currentOffset += len(lengthPrefixString(name))
|
|
currentOffset += len(encodeNumber(index))
|
|
currentOffset += len(getBookPayloadRecord(name, index))
|
|
newRecord.append([offset,bookHeaderRecords[name][index][1],bookHeaderRecords[name][index][2]])
|
|
|
|
headerData.append([name,newRecord])
|
|
|
|
|
|
|
|
return headerData
|
|
|
|
#
|
|
# Create decrypted book
|
|
#
|
|
|
|
def createDecryptedBook(outputFile):
|
|
outputFile = open(outputFile,"wb")
|
|
# Write the payload in a temporary file
|
|
headerData = createDecryptedPayload(None)
|
|
outputFile.write("TPZ0")
|
|
outputFile.write(encodeNumber(len(headerData)))
|
|
|
|
for header in headerData :
|
|
outputFile.write(chr(0x63))
|
|
outputFile.write(lengthPrefixString(header[0]))
|
|
outputFile.write(encodeNumber(len(header[1])))
|
|
for numbers in header[1] :
|
|
outputFile.write(encodeNumber(numbers[0]))
|
|
outputFile.write(encodeNumber(numbers[1]))
|
|
outputFile.write(encodeNumber(numbers[2]))
|
|
|
|
outputFile.write(chr(0x64))
|
|
createDecryptedPayload(outputFile)
|
|
outputFile.close()
|
|
|
|
#
|
|
# 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("\nCMBDTC.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 Saves a decrypted copy of the book")
|
|
print("-r Prints or writes to disk a record indicated in the form name:index (e.g \"img:0\")")
|
|
print("-o Output file name to write records and decrypted books")
|
|
print("-v Verbose (can be used several times)")
|
|
print("-i Prints kindle.info database")
|
|
|
|
#
|
|
# Main
|
|
#
|
|
|
|
def main(argv=sys.argv):
|
|
global kindleDatabase
|
|
global bookMetadata
|
|
global bookKey
|
|
global bookFile
|
|
global command
|
|
|
|
progname = os.path.basename(argv[0])
|
|
|
|
verbose = 0
|
|
recordName = ""
|
|
recordIndex = 0
|
|
outputFile = ""
|
|
PIDs = []
|
|
kindleDatabase = None
|
|
command = ""
|
|
|
|
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], "vdir:o:p:")
|
|
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 == "-i":
|
|
setCommand("printInfo")
|
|
if o =="-o":
|
|
if a == None :
|
|
raise CMBDTCFatal("Invalid parameter for -o")
|
|
outputFile = a
|
|
if o =="-r":
|
|
setCommand("printRecord")
|
|
try:
|
|
recordName,recordIndex = a.split(':')
|
|
except:
|
|
raise CMBDTCFatal("Invalid parameter for -r")
|
|
if o =="-p":
|
|
PIDs.append(a)
|
|
if o =="-d":
|
|
setCommand("doit")
|
|
|
|
if command == "" :
|
|
raise CMBDTCFatal("No action supplied on command line")
|
|
|
|
#
|
|
# Read the encrypted database
|
|
#
|
|
|
|
try:
|
|
kindleDatabase = parseKindleInfo()
|
|
except Exception, message:
|
|
if verbose>0:
|
|
print(message)
|
|
|
|
if kindleDatabase != None :
|
|
if command == "printInfo" :
|
|
printKindleInfo()
|
|
|
|
#
|
|
# Compute the DSN
|
|
#
|
|
|
|
# Get the Mazama Random number
|
|
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
|
|
|
# Get the HDD serial
|
|
encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1)
|
|
|
|
# Get the current user name
|
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
|
|
|
# concat, hash and encode
|
|
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
|
|
|
if verbose >1:
|
|
print("DSN: " + DSN)
|
|
|
|
#
|
|
# Compute the device PID
|
|
#
|
|
|
|
table = generatePidEncryptionTable()
|
|
devicePID = generateDevicePID(table,DSN,4)
|
|
PIDs.append(devicePID)
|
|
|
|
if verbose > 0:
|
|
print("Device PID: " + devicePID)
|
|
|
|
#
|
|
# Open book and parse metadata
|
|
#
|
|
|
|
if len(args) == 1:
|
|
|
|
bookFile = openBook(args[0])
|
|
parseTopazHeader()
|
|
parseMetadata()
|
|
|
|
#
|
|
# Compute book PID
|
|
#
|
|
|
|
# Get the account token
|
|
|
|
if kindleDatabase != None:
|
|
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
|
|
|
if verbose >1:
|
|
print("Account Token: " + kindleAccountToken)
|
|
|
|
keysRecord = bookMetadata["keys"]
|
|
keysRecordRecord = bookMetadata[keysRecord]
|
|
|
|
pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord)
|
|
|
|
bookPID = encodePID(pidHash)
|
|
PIDs.append(bookPID)
|
|
|
|
if verbose > 0:
|
|
print ("Book PID: " + bookPID )
|
|
|
|
#
|
|
# 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 outputFile!="" :
|
|
createDecryptedBook(outputFile)
|
|
if verbose >0 :
|
|
print ("Decrypted book saved. Don't pirate!")
|
|
elif verbose > 0:
|
|
print("Output file name was not supplied.")
|
|
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|
|
|