]> xmof Git - DeDRM.git/commitdiff
First Topaz decryption by CMBDTC at openrce.org
authorCMBDTC <cmbdtc@openrce.org>
Tue, 29 Dec 2009 12:01:13 +0000 (12:01 +0000)
committerApprentice Alf <apprenticealf@gmail.com>
Sat, 28 Feb 2015 12:01:49 +0000 (12:01 +0000)
Topaz_Tools/lib/CMBDTC.py [new file with mode: 0644]

diff --git a/Topaz_Tools/lib/CMBDTC.py b/Topaz_Tools/lib/CMBDTC.py
new file mode 100644 (file)
index 0000000..45219df
--- /dev/null
@@ -0,0 +1,724 @@
+#! /usr/bin/python
+
+"""
+
+Comprehensive Mazama Book DRM with Topaz Cryptography V1.0
+
+-----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
+
+#
+# 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 SHA1 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 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 ("--------------------------\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 form 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
+  
+#
+# Get a length prefixed string from the file 
+#
+
+def bookReadString():
+    stringLength = bookReadEncodedNumber()
+    return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0]  
+    
+#
+# Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...]
+#
+    
+def bookReadHeaderRecordData():
+    nbValues = ord(bookFile.read(1))
+    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 = ord(bookFile.read(1))
+    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")
+            
+    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
+# 
+# Program usage
+#
+   
+def usage():
+    print("\nUsage:")
+    print("\nCMBDTC.py [options] bookFileName\n")
+    print("-r prints a record indicated in the form name:index (e.g \"img:0\")")
+    print("-o Output file name to write records")
+    print("-v verbose (can be used several times)")
+    print("-i print kindle.info database")
+#
+# Main
+#   
+
+def main(argv=sys.argv):
+    global kindleDatabase
+    global bookMetadata
+    global bookKey
+    global bookFile
+    progname = os.path.basename(argv[0])
+    
+    verbose = 0
+    printInfo = False
+    recordName = ""
+    recordIndex = 0
+    outputFile = ""
+    
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "vir:o:")
+    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":
+            printInfo = True
+        if o =="-o":
+            outputFile = a
+        if o =="-r":
+            recordName,recordIndex = a.split(':')
+   
+    #
+    # Read the encrypted database
+    #
+    
+    kindleDatabase = parseKindleInfo()
+    
+    if 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)
+    
+    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
+        kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
+    
+        if verbose >1:
+            print("Account Token: " + kindleAccountToken)
+
+        keysRecord = bookMetadata["keys"]
+        keysRecordRecord = bookMetadata[keysRecord]
+    
+        pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord)
+   
+        PID = encodePID(pidHash)
+    
+        if verbose > 0:
+            print ("Book PID: " + PID )
+    
+    #
+    #  Decrypt book key
+    #
+    
+        dkey = getBookPayloadRecord('dkey', 0) 
+     
+        bookKey = decryptDkeyRecords(dkey,PID)[0]
+        
+        if verbose > 0:
+           print("Book key: " + bookKey.encode('hex'))
+    
+        if recordName != "" :
+            extractBookPayloadRecord(recordName,int(recordIndex),outputFile)
+            if outputFile != "" and verbose>0 :
+                 print("Wrote record to file: "+outputFile) 
+    
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())