parent
bc968f8eca
commit
8b632e309f
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,900 +0,0 @@
|
|||||||
#! /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())
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540
|
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350
|
||||||
{\fonttbl}
|
{\fonttbl}
|
||||||
{\colortbl;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;}
|
||||||
}
|
}
|
@ -1,145 +0,0 @@
|
|||||||
#! /usr/bin/python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
# For use with Topaz Scripts Version 2.6
|
|
||||||
|
|
||||||
class Unbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
def write(self, data):
|
|
||||||
self.stream.write(data)
|
|
||||||
self.stream.flush()
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
import os, getopt
|
|
||||||
|
|
||||||
# local routines
|
|
||||||
import convert2xml
|
|
||||||
import flatxml2html
|
|
||||||
import decode_meta
|
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
|
||||||
print 'Usage: '
|
|
||||||
print ' '
|
|
||||||
print ' genxml.py dict0000.dat unencryptedBookDir'
|
|
||||||
print ' '
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
|
||||||
bookDir = ''
|
|
||||||
|
|
||||||
if len(argv) == 0:
|
|
||||||
argv = sys.argv
|
|
||||||
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(argv[1:], "h:")
|
|
||||||
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if len(opts) == 0 and len(args) == 0 :
|
|
||||||
usage()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for o, a in opts:
|
|
||||||
if o =="-h":
|
|
||||||
usage()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
bookDir = args[0]
|
|
||||||
|
|
||||||
if not os.path.exists(bookDir) :
|
|
||||||
print "Can not find directory with unencrypted book"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
dictFile = os.path.join(bookDir,'dict0000.dat')
|
|
||||||
if not os.path.exists(dictFile) :
|
|
||||||
print "Can not find dict0000.dat file"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
pageDir = os.path.join(bookDir,'page')
|
|
||||||
if not os.path.exists(pageDir) :
|
|
||||||
print "Can not find page directory in unencrypted book"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
glyphsDir = os.path.join(bookDir,'glyphs')
|
|
||||||
if not os.path.exists(glyphsDir) :
|
|
||||||
print "Can not find glyphs directory in unencrypted book"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
otherFile = os.path.join(bookDir,'other0000.dat')
|
|
||||||
if not os.path.exists(otherFile) :
|
|
||||||
print "Can not find other0000.dat in unencrypted book"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
|
||||||
if not os.path.exists(metaFile) :
|
|
||||||
print "Can not find metadata0000.dat in unencrypted book"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
xmlDir = os.path.join(bookDir,'xml')
|
|
||||||
if not os.path.exists(xmlDir):
|
|
||||||
os.makedirs(xmlDir)
|
|
||||||
|
|
||||||
|
|
||||||
print 'Processing ... '
|
|
||||||
|
|
||||||
print ' ', 'metadata0000.dat'
|
|
||||||
fname = os.path.join(bookDir,'metadata0000.dat')
|
|
||||||
xname = os.path.join(xmlDir, 'metadata.txt')
|
|
||||||
metastr = decode_meta.getMetaData(fname)
|
|
||||||
file(xname, 'wb').write(metastr)
|
|
||||||
|
|
||||||
print ' ', 'other0000.dat'
|
|
||||||
fname = os.path.join(bookDir,'other0000.dat')
|
|
||||||
xname = os.path.join(xmlDir, 'stylesheet.xml')
|
|
||||||
pargv=[]
|
|
||||||
pargv.append('convert2xml.py')
|
|
||||||
pargv.append(dictFile)
|
|
||||||
pargv.append(fname)
|
|
||||||
xmlstr = convert2xml.main(pargv)
|
|
||||||
file(xname, 'wb').write(xmlstr)
|
|
||||||
|
|
||||||
filenames = os.listdir(pageDir)
|
|
||||||
filenames = sorted(filenames)
|
|
||||||
|
|
||||||
for filename in filenames:
|
|
||||||
print ' ', filename
|
|
||||||
fname = os.path.join(pageDir,filename)
|
|
||||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
|
||||||
pargv=[]
|
|
||||||
pargv.append('convert2xml.py')
|
|
||||||
pargv.append(dictFile)
|
|
||||||
pargv.append(fname)
|
|
||||||
xmlstr = convert2xml.main(pargv)
|
|
||||||
file(xname, 'wb').write(xmlstr)
|
|
||||||
|
|
||||||
filenames = os.listdir(glyphsDir)
|
|
||||||
filenames = sorted(filenames)
|
|
||||||
|
|
||||||
for filename in filenames:
|
|
||||||
print ' ', filename
|
|
||||||
fname = os.path.join(glyphsDir,filename)
|
|
||||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
|
||||||
pargv=[]
|
|
||||||
pargv.append('convert2xml.py')
|
|
||||||
pargv.append(dictFile)
|
|
||||||
pargv.append(fname)
|
|
||||||
xmlstr = convert2xml.main(pargv)
|
|
||||||
file(xname, 'wb').write(xmlstr)
|
|
||||||
|
|
||||||
|
|
||||||
print 'Processing Complete'
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main(''))
|
|
Loading…
Reference in New Issue