@ -4,7 +4,7 @@
from __future__ import with_statement
# kindlekey.py
# Copyright © 2008-20 17 Apprentice Harper et al.
# Copyright © 2008-20 20 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 Artis i tic License Version 1.0
CryptoPy Artis tic 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
@ -1425,6 +1449,18 @@ elif isosx:
kInfoFiles = [ ]
found = False
home = os . getenv ( ' HOME ' )
# check for .kinf2018 file in new location (App Store Kindle for Mac)
testpath = home + ' /Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2018 '
if os . path . isfile ( testpath ) :
kInfoFiles . append ( testpath )
print ( ' Found k4Mac kinf2018 file: ' + testpath )
found = True
# check for .kinf2018 files
testpath = home + ' /Library/Application Support/Kindle/storage/.kinf2018 '
if os . path . isfile ( testpath ) :
kInfoFiles . append ( testpath )
print ( ' Found k4Mac kinf2018 file: ' + testpath )
found = True
# check for .kinf2011 file in new location (App Store Kindle for Mac)
testpath = home + ' /Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011 '
if os . path . isfile ( testpath ) :
@ -1505,12 +1541,21 @@ elif isosx:
cleartext = UnprotectHeaderData ( encryptedValue )
# now extract the pieces in the same way
# this version is different from K4PC it scales the build number by multipying by 735
pattern = re . compile ( r ''' \ [Version:( \ d+) \ ] \ [Build:( \ d+) \ ] \ [Cksum:([^ \ ]]+) \ ] \ [Guid:([ \ { \ }a-z0-9 \ -]+) \ ] ''' , re . IGNORECASE )
for m in re . finditer ( pattern , cleartext ) :
entropy = str ( int ( m . group ( 2 ) ) * 0x2df ) + m . group ( 4 )
version = int ( m . group ( 1 ) )
build = m . group ( 2 )
guid = m . group ( 4 )
cud = CryptUnprotectData ( entropy , IDString )
if version == 5 : # .kinf2011: identical to K4PC, except the build number gets multiplied
entropy = str ( 0x2df * int ( build ) ) + guid
cud = CryptUnprotectData ( entropy , IDString )
elif version == 6 : # .kinf2018: identical to K4PC
salt = str ( 0x6d8 * int ( build ) ) + guid
sp = GetUserName ( ) + ' +@#$ % + ' + IDString
passwd = encode ( SHA256 ( sp ) , charMap5 )
key = LibCrypto ( ) . keyivgen ( passwd , salt , 10000 , 0x400 ) [ : 32 ]
# loop through the item records until all are processed
while len ( items ) > 0 :
@ -1571,9 +1616,28 @@ elif isosx:
encdata = encdata [ noffset : ]
encdata = encdata + pfx
# decode using testMap8 to get the CryptProtect Data
encryptedValue = decode ( encdata , testMap8 )
cleartext = cud . decrypt ( encryptedValue )
if version == 5 :
# decode using testMap8 to get the CryptProtect Data
encryptedValue = decode ( encdata , testMap8 )
cleartext = cud . decrypt ( encryptedValue )
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 )
# print keyname
# print cleartext
if len ( cleartext ) > 0 :