|
|
|
@ -4,7 +4,7 @@
|
|
|
|
|
from __future__ import with_statement
|
|
|
|
|
|
|
|
|
|
# kindlekey.py
|
|
|
|
|
# Copyright © 2008-2017 Apprentice Harper et al.
|
|
|
|
|
# Copyright © 2008-2020 Apprentice Harper et al.
|
|
|
|
|
|
|
|
|
|
__license__ = 'GPL v3'
|
|
|
|
|
__version__ = '2.6'
|
|
|
|
@ -29,6 +29,7 @@ __version__ = '2.6'
|
|
|
|
|
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
|
|
|
|
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
|
|
|
|
# 2.6 - Start adding support for Kindle 1.25+ .kinf2018 file
|
|
|
|
|
# 2.7 - Finish .kinf2018 support
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
@ -36,7 +37,7 @@ Retrieve Kindle for PC/Mac user key.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import sys, os, re
|
|
|
|
|
from struct import pack, unpack, unpack_from
|
|
|
|
|
from struct import pack, unpack
|
|
|
|
|
import json
|
|
|
|
|
import getopt
|
|
|
|
|
|
|
|
|
@ -207,7 +208,7 @@ if iswindows:
|
|
|
|
|
Original Version
|
|
|
|
|
Copyright (c) 2002 by Paul A. Lambert
|
|
|
|
|
Under:
|
|
|
|
|
CryptoPy Artisitic License Version 1.0
|
|
|
|
|
CryptoPy Artistic License Version 1.0
|
|
|
|
|
See the wonderful pure python package cryptopy-1.2.5
|
|
|
|
|
and read its LICENSE.txt for complete license details.
|
|
|
|
|
"""
|
|
|
|
@ -1050,7 +1051,7 @@ if iswindows:
|
|
|
|
|
DB = {}
|
|
|
|
|
with open(kInfoFile, 'rb') as infoReader:
|
|
|
|
|
data = infoReader.read()
|
|
|
|
|
# assume newest .kinf2011 style .kinf file
|
|
|
|
|
# assume .kinf2011 or .kinf2018 style .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]
|
|
|
|
@ -1064,8 +1065,17 @@ if iswindows:
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
version = int(m.group(1))
|
|
|
|
|
build = m.group(2)
|
|
|
|
|
guid = m.group(4)
|
|
|
|
|
|
|
|
|
|
if version == 5: # .kinf2011
|
|
|
|
|
added_entropy = build + guid
|
|
|
|
|
elif version == 6: # .kinf2018
|
|
|
|
|
salt = str(0x6d8 * int(build)) + guid
|
|
|
|
|
sp = GetUserName() + '+@#$%+' + GetIDString()
|
|
|
|
|
passwd = encode(SHA256(sp), charMap5)
|
|
|
|
|
key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow
|
|
|
|
|
|
|
|
|
|
# loop through the item records until all are processed
|
|
|
|
|
while len(items) > 0:
|
|
|
|
@ -1077,10 +1087,6 @@ if iswindows:
|
|
|
|
|
# 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
|
|
|
|
@ -1128,11 +1134,29 @@ if iswindows:
|
|
|
|
|
encdata = encdata + pfx
|
|
|
|
|
#print "rearranged data:",encdata
|
|
|
|
|
|
|
|
|
|
if version == 5:
|
|
|
|
|
# decode using new testMap8 to get the original CryptProtect Data
|
|
|
|
|
encryptedValue = decode(encdata,testMap8)
|
|
|
|
|
#print "decoded data:",encryptedValue.encode('hex')
|
|
|
|
|
entropy = SHA1(keyhash) + added_entropy
|
|
|
|
|
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
|
|
|
|
elif version == 6:
|
|
|
|
|
from Crypto.Cipher import AES
|
|
|
|
|
from Crypto.Util import Counter
|
|
|
|
|
# decode using new testMap8 to get IV + ciphertext
|
|
|
|
|
iv_ciphertext = decode(encdata, testMap8)
|
|
|
|
|
# pad IV so that we can substitute AES-CTR for GCM
|
|
|
|
|
iv = iv_ciphertext[:12] + b'\x00\x00\x00\x02'
|
|
|
|
|
ciphertext = iv_ciphertext[12:]
|
|
|
|
|
# convert IV to int for use with pycrypto
|
|
|
|
|
iv_ints = unpack('>QQ', iv)
|
|
|
|
|
iv = iv_ints[0] << 64 | iv_ints[1]
|
|
|
|
|
# set up AES-CTR
|
|
|
|
|
ctr = Counter.new(128, initial_value=iv)
|
|
|
|
|
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
|
|
|
|
|
# decrypt and decode
|
|
|
|
|
cleartext = decode(cipher.decrypt(ciphertext), charMap5)
|
|
|
|
|
|
|
|
|
|
# decode using new testMap8 to get the original CryptProtect Data
|
|
|
|
|
encryptedValue = decode(encdata,testMap8)
|
|
|
|
|
#print "decoded data:",encryptedValue.encode('hex')
|
|
|
|
|
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
|
|
|
|
|
if len(cleartext)>0:
|
|
|
|
|
#print "cleartext data:",cleartext,":end data"
|
|
|
|
|
DB[keyname] = cleartext
|
|
|
|
|