DeDRM_tools/Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py

433 lines
15 KiB
Python

#!/usr/bin/env python
# K4PC Windows specific routines
from __future__ import with_statement
import sys, os, re
from struct import pack, unpack, unpack_from
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
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
import traceback
# crypto digestroutines
import hashlib
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
# For K4PC 1.9.X
# use routines in alfcrypto:
# AES_cbc_encrypt
# AES_set_decrypt_key
# PKCS5_PBKDF2_HMAC_SHA1
from alfcrypto import AES_CBC, KeyIVGen
def UnprotectHeaderData(encryptedData):
passwdData = 'header_key_data'
salt = 'HEADER.2011'
iter = 0x80
keylen = 0x100
key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
key = key_iv[0:32]
iv = key_iv[32:48]
aes=AES_CBC()
aes.set_decrypt_key(key, iv)
cleartext = aes.decrypt(encryptedData)
return cleartext
# simple primes table (<= n) calculator
def primes(n):
if n==2: return [2]
elif n<2: return []
s=range(3,n+1,2)
mroot = n ** 0.5
half=(n+1)/2-1
i=0
m=3
while m <= mroot:
if s[i]:
j=(m*m-3)/2
s[j]=0
while j<half:
s[j]=0
j+=m
i=i+1
m=2*i+3
return [2]+[x for x in s if x]
# Various character maps used to decrypt kindle info values.
# Probably supposed to act as obfuscation
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
# New maps in K4PC 1.9.0
testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG"
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
class DrmException(Exception):
pass
# Encode the bytes in data with the characters in map
def encode(data, map):
result = ""
for char in data:
value = ord(char)
Q = (value ^ 0x80) // len(map)
R = value % len(map)
result += map[Q]
result += map[R]
return result
# Hash the bytes in data and then encode the digest with the characters in map
def encodeHash(data,map):
return encode(MD5(data),map)
# Decode the string in data with the characters in map. Returns the decoded bytes
def decode(data,map):
result = ""
for i in range (0,len(data)-1,2):
high = map.find(data[i])
low = map.find(data[i+1])
if (high == -1) or (low == -1) :
break
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
result += pack("B",value)
return result
# interface with Windows OS Routines
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 = GetSystemDirectory().split('\\')[0] + '\\'):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return str(vsn.value)
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetIDString():
return GetVolumeSerialNumber()
def getLastError():
GetLastError = kernel32.GetLastError
GetLastError.argtypes = None
GetLastError.restype = c_uint
def getLastError():
return GetLastError()
return getLastError
getLastError = getLastError()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(2)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
errcd = getLastError()
if errcd == 234:
# bad wine implementation up through wine 1.3.21
return "AlternateUserName"
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, flags):
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, flags, byref(outdata)):
# raise DrmException("Failed to Unprotect Data")
return 'failed'
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
# Locate all of the kindle-info style files and return as list
def getKindleInfoFiles(kInfoFiles):
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
# some 64 bit machines do not have the proper registry key for some reason
# or the pythonn interface to the 32 vs 64 bit registry is broken
if 'LOCALAPPDATA' in os.environ.keys():
path = os.environ['LOCALAPPDATA']
print "searching for kinfoFiles in ", path
# first look for older kindle-info files
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
if not os.path.isfile(kinfopath):
print('No kindle.info files have not been found.')
else:
kInfoFiles.append(kinfopath)
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
if not os.path.isfile(kinfopath):
print('No K4PC 1.5.X .kinf files have not been found.')
else:
kInfoFiles.append(kinfopath)
# now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
if not os.path.isfile(kinfopath):
print('No K4PC 1.6.X .kinf files have not been found.')
else:
kInfoFiles.append(kinfopath)
# now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
if not os.path.isfile(kinfopath):
print('No K4PC 1.9.X .kinf files have not been found.')
else:
kInfoFiles.append(kinfopath)
return kInfoFiles
# determine type of kindle info provided and return a
# database of keynames and values
def getDBfromFile(kInfoFile):
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
DB = {}
cnt = 0
infoReader = open(kInfoFile, 'r')
hdr = infoReader.read(1)
data = infoReader.read()
if data.find('{') != -1 :
# older style kindle-info file
items = data.split('{')
for item in items:
if item != '':
keyhash, rawdata = item.split(':')
keyname = "unknown"
for name in names:
if encodeHash(name,charMap2) == keyhash:
keyname = name
break
if keyname == "unknown":
keyname = keyhash
encryptedValue = decode(rawdata,charMap2)
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
cnt = cnt + 1
if cnt == 0:
DB = None
return DB
if hdr == '/':
# else rainier-2-1-1 .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
data = data[:-1]
items = data.split('/')
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
item = items.pop(0)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
# the raw keyhash string is used to create entropy for the actual
# CryptProtectData Blob that represents that keys contents
entropy = SHA1(keyhash)
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
keyname = "unknown"
for name in names:
if encodeHash(name,charMap5) == keyhash:
keyname = name
break
if keyname == "unknown":
keyname = keyhash
# the charMap5 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using charMap5 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the charMap5 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by charMap5
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
# now properly split and recombine
# by moving noffset chars from the start of the
# string to the end of the string
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using Map5 to get the CryptProtect Data
encryptedValue = decode(encdata,charMap5)
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
cnt = cnt + 1
if cnt == 0:
DB = None
return DB
# else newest .kinf2011 style .kinf file
# the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split
# need to put back the first char read because it it part
# of the added entropy blob
data = hdr + data[:-1]
items = data.split('/')
# starts with and encoded and encrypted header blob
headerblob = items.pop(0)
encryptedValue = decode(headerblob, testMap1)
cleartext = UnprotectHeaderData(encryptedValue)
# now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext):
added_entropy = m.group(2) + m.group(4)
# loop through the item records until all are processed
while len(items) > 0:
# get the first item record
item = items.pop(0)
# the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32]
# the sha1 of raw keyhash string is used to create entropy along
# with the added entropy provided above from the headerblob
entropy = SHA1(keyhash) + added_entropy
# the remainder of the first record when decoded with charMap5
# has the ':' split char followed by the string representation
# of the number of records that follow
# and make up the contents
srcnt = decode(item[34:],charMap5)
rcnt = int(srcnt)
# read and store in rcnt records of data
# that make up the contents value
edlst = []
for i in xrange(rcnt):
item = items.pop(0)
edlst.append(item)
# key names now use the new testMap8 encoding
keyname = "unknown"
for name in names:
if encodeHash(name,testMap8) == keyhash:
keyname = name
break
# the testMap8 encoded contents data has had a length
# of chars (always odd) cut off of the front and moved
# to the end to prevent decoding using testMap8 from
# working properly, and thereby preventing the ensuing
# CryptUnprotectData call from succeeding.
# The offset into the testMap8 encoded contents seems to be:
# len(contents)-largest prime number <= int(len(content)/3)
# (in other words split "about" 2/3rds of the way through)
# move first offsets chars to end to align for decode by testMap8
# by moving noffset chars from the start of the
# string to the end of the string
encdata = "".join(edlst)
contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1]
pfx = encdata[0:noffset]
encdata = encdata[noffset:]
encdata = encdata + pfx
# decode using new testMap8 to get the original CryptProtect Data
encryptedValue = decode(encdata,testMap8)
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
DB[keyname] = cleartext
cnt = cnt + 1
if cnt == 0:
DB = None
return DB