diff --git a/DeDRM_plugin/ion.py b/DeDRM_plugin/ion.py index c361d20..5645056 100644 --- a/DeDRM_plugin/ion.py +++ b/DeDRM_plugin/ion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- 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 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.EncryptedPage@2.0', '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): ion.addtocatalog("ProtectedData", 1, SYM_NAMES) @@ -741,8 +742,42 @@ def pkcs7unpad(msg, blocklen): 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): envelope = None + version = None voucher = None drmkey = None license_type = "Unknown" @@ -777,9 +812,9 @@ class DrmIonVoucher(object): else: _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]) b = aes.decrypt(self.ciphertext) b = pkcs7unpad(b, 16) @@ -814,8 +849,9 @@ class DrmIonVoucher(object): def parse(self): self.envelope.reset() _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") + self.version = int(self.envelope.gettypename().split('@')[1][:-2]) self.envelope.stepin() while self.envelope.hasnext(): diff --git a/DeDRM_plugin/kindlekey.py b/DeDRM_plugin/kindlekey.py index 7b994ed..9a299e6 100644 --- a/DeDRM_plugin/kindlekey.py +++ b/DeDRM_plugin/kindlekey.py @@ -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 @@ -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: