Merge pull request #1037 from apprenticesakuya/master
Finish .kinf2018 support and add KFX v2/v3 support
This commit is contained in:
commit
885ef5e890
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
|
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon. VoucherEnvelope v2/v3 support by apprenticesakuya.
|
||||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
@ -719,7 +719,8 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
||||||
'com.amazon.drm.EnvelopeMetadata@2.0',
|
'com.amazon.drm.EnvelopeMetadata@2.0',
|
||||||
'com.amazon.drm.EncryptedPage@2.0',
|
'com.amazon.drm.EncryptedPage@2.0',
|
||||||
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
||||||
'com.amazon.drm.Compressed@1.0', 'priority', 'refines']
|
'com.amazon.drm.Compressed@1.0', 'page_index_table',
|
||||||
|
'com.amazon.drm.VoucherEnvelope@2.0', 'com.amazon.drm.VoucherEnvelope@3.0' ]
|
||||||
|
|
||||||
def addprottable(ion):
|
def addprottable(ion):
|
||||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||||
|
@ -741,8 +742,42 @@ def pkcs7unpad(msg, blocklen):
|
||||||
return msg[:-paddinglen]
|
return msg[:-paddinglen]
|
||||||
|
|
||||||
|
|
||||||
|
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
|
||||||
|
VOUCHER_VERSION_INFOS = {
|
||||||
|
2: [b'Antidisestablishmentarianism', 5],
|
||||||
|
3: [b'Floccinaucinihilipilification', 8]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# obfuscate shared secret according to the VoucherEnvelope version
|
||||||
|
def obfuscate(secret, version):
|
||||||
|
if version == 1: # v1 does not use obfuscation
|
||||||
|
return secret
|
||||||
|
|
||||||
|
params = VOUCHER_VERSION_INFOS[version]
|
||||||
|
word = params[0]
|
||||||
|
magic = params[1]
|
||||||
|
|
||||||
|
# extend secret so that its length is divisible by the magic number
|
||||||
|
if len(secret) % magic != 0:
|
||||||
|
secret = secret + b'\x00' * (magic - len(secret) % magic)
|
||||||
|
|
||||||
|
secret = bytearray(secret)
|
||||||
|
|
||||||
|
obfuscated = bytearray(len(secret))
|
||||||
|
wordhash = bytearray(hashlib.sha256(word).digest())
|
||||||
|
|
||||||
|
# shuffle secret and xor it with the first half of the word hash
|
||||||
|
for i in range(0, len(secret)):
|
||||||
|
index = i // (len(secret) // magic) + magic * (i % (len(secret) // magic))
|
||||||
|
obfuscated[index] = secret[i] ^ wordhash[index % 16]
|
||||||
|
|
||||||
|
return obfuscated
|
||||||
|
|
||||||
|
|
||||||
class DrmIonVoucher(object):
|
class DrmIonVoucher(object):
|
||||||
envelope = None
|
envelope = None
|
||||||
|
version = None
|
||||||
voucher = None
|
voucher = None
|
||||||
drmkey = None
|
drmkey = None
|
||||||
license_type = "Unknown"
|
license_type = "Unknown"
|
||||||
|
@ -777,9 +812,9 @@ class DrmIonVoucher(object):
|
||||||
else:
|
else:
|
||||||
_assert(False, "Unknown lock parameter: %s" % param)
|
_assert(False, "Unknown lock parameter: %s" % param)
|
||||||
|
|
||||||
sharedsecret = shared.encode("UTF-8")
|
sharedsecret = obfuscate(shared.encode('ASCII'), self.version)
|
||||||
|
|
||||||
key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest()
|
key = hmac.new(sharedsecret, "PIDv3", digestmod=hashlib.sha256).digest()
|
||||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||||
b = aes.decrypt(self.ciphertext)
|
b = aes.decrypt(self.ciphertext)
|
||||||
b = pkcs7unpad(b, 16)
|
b = pkcs7unpad(b, 16)
|
||||||
|
@ -814,8 +849,9 @@ class DrmIonVoucher(object):
|
||||||
def parse(self):
|
def parse(self):
|
||||||
self.envelope.reset()
|
self.envelope.reset()
|
||||||
_assert(self.envelope.hasnext(), "Envelope is empty")
|
_assert(self.envelope.hasnext(), "Envelope is empty")
|
||||||
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
|
_assert(self.envelope.next() == TID_STRUCT and str.startswith(self.envelope.gettypename(), "com.amazon.drm.VoucherEnvelope@"),
|
||||||
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
||||||
|
self.version = int(self.envelope.gettypename().split('@')[1][:-2])
|
||||||
|
|
||||||
self.envelope.stepin()
|
self.envelope.stepin()
|
||||||
while self.envelope.hasnext():
|
while self.envelope.hasnext():
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
# kindlekey.py
|
# kindlekey.py
|
||||||
# Copyright © 2008-2017 Apprentice Harper et al.
|
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '2.6'
|
__version__ = '2.6'
|
||||||
|
@ -29,6 +29,7 @@ __version__ = '2.6'
|
||||||
# 2.4 - Fix for complex Mac disk setups, thanks to Tibs
|
# 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.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.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
|
import sys, os, re
|
||||||
from struct import pack, unpack, unpack_from
|
from struct import pack, unpack
|
||||||
import json
|
import json
|
||||||
import getopt
|
import getopt
|
||||||
|
|
||||||
|
@ -207,7 +208,7 @@ if iswindows:
|
||||||
Original Version
|
Original Version
|
||||||
Copyright (c) 2002 by Paul A. Lambert
|
Copyright (c) 2002 by Paul A. Lambert
|
||||||
Under:
|
Under:
|
||||||
CryptoPy Artisitic License Version 1.0
|
CryptoPy Artistic License Version 1.0
|
||||||
See the wonderful pure python package cryptopy-1.2.5
|
See the wonderful pure python package cryptopy-1.2.5
|
||||||
and read its LICENSE.txt for complete license details.
|
and read its LICENSE.txt for complete license details.
|
||||||
"""
|
"""
|
||||||
|
@ -1050,7 +1051,7 @@ if iswindows:
|
||||||
DB = {}
|
DB = {}
|
||||||
with open(kInfoFile, 'rb') as infoReader:
|
with open(kInfoFile, 'rb') as infoReader:
|
||||||
data = infoReader.read()
|
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
|
# the .kinf file uses "/" to separate it into records
|
||||||
# so remove the trailing "/" to make it easy to use split
|
# so remove the trailing "/" to make it easy to use split
|
||||||
data = data[:-1]
|
data = data[:-1]
|
||||||
|
@ -1064,8 +1065,17 @@ if iswindows:
|
||||||
# now extract the pieces that form the added entropy
|
# now extract the pieces that form the added entropy
|
||||||
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||||
for m in re.finditer(pattern, cleartext):
|
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
|
# loop through the item records until all are processed
|
||||||
while len(items) > 0:
|
while len(items) > 0:
|
||||||
|
@ -1077,10 +1087,6 @@ if iswindows:
|
||||||
# is the MD5 hash of the key name encoded by charMap5
|
# is the MD5 hash of the key name encoded by charMap5
|
||||||
keyhash = item[0:32]
|
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
|
# the remainder of the first record when decoded with charMap5
|
||||||
# has the ':' split char followed by the string representation
|
# has the ':' split char followed by the string representation
|
||||||
# of the number of records that follow
|
# of the number of records that follow
|
||||||
|
@ -1128,11 +1134,29 @@ if iswindows:
|
||||||
encdata = encdata + pfx
|
encdata = encdata + pfx
|
||||||
#print "rearranged data:",encdata
|
#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:
|
if len(cleartext)>0:
|
||||||
#print "cleartext data:",cleartext,":end data"
|
#print "cleartext data:",cleartext,":end data"
|
||||||
DB[keyname] = cleartext
|
DB[keyname] = cleartext
|
||||||
|
@ -1425,6 +1449,18 @@ elif isosx:
|
||||||
kInfoFiles=[]
|
kInfoFiles=[]
|
||||||
found = False
|
found = False
|
||||||
home = os.getenv('HOME')
|
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)
|
# 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'
|
testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011'
|
||||||
if os.path.isfile(testpath):
|
if os.path.isfile(testpath):
|
||||||
|
@ -1505,12 +1541,21 @@ elif isosx:
|
||||||
cleartext = UnprotectHeaderData(encryptedValue)
|
cleartext = UnprotectHeaderData(encryptedValue)
|
||||||
|
|
||||||
# now extract the pieces in the same way
|
# 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)
|
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
|
||||||
for m in re.finditer(pattern, cleartext):
|
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
|
# loop through the item records until all are processed
|
||||||
while len(items) > 0:
|
while len(items) > 0:
|
||||||
|
@ -1571,9 +1616,28 @@ elif isosx:
|
||||||
encdata = encdata[noffset:]
|
encdata = encdata[noffset:]
|
||||||
encdata = encdata + pfx
|
encdata = encdata + pfx
|
||||||
|
|
||||||
# decode using testMap8 to get the CryptProtect Data
|
if version == 5:
|
||||||
encryptedValue = decode(encdata,testMap8)
|
# decode using testMap8 to get the CryptProtect Data
|
||||||
cleartext = cud.decrypt(encryptedValue)
|
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 keyname
|
||||||
# print cleartext
|
# print cleartext
|
||||||
if len(cleartext) > 0:
|
if len(cleartext) > 0:
|
||||||
|
|
Loading…
Reference in New Issue