From a1dd63ae5f48d8320b94efc79ffc2fc8e829988e Mon Sep 17 00:00:00 2001 From: a980e066a01 <100724039+a980e066a01@users.noreply.github.com> Date: Tue, 22 Feb 2022 23:16:03 +0000 Subject: [PATCH] Remove OpenSSL support; only support PyCryptodome This allows us to clean up the code a lot. On Windows, it isn't installed by default and most of the time not be found at all. On M1 Macs, the kernel will kill the process instead. Closes #33. --- DeDRM_plugin/adobekey.py | 109 +--- DeDRM_plugin/adobekey_get_passhash.py | 15 +- DeDRM_plugin/androidkindlekey.py | 41 +- DeDRM_plugin/erdr2pml.py | 60 +- DeDRM_plugin/ignoblekeyAndroid.py | 14 +- DeDRM_plugin/ignoblekeyGenPassHash.py | 106 +--- DeDRM_plugin/ignoblekeyWindowsStore.py | 13 +- DeDRM_plugin/ineptepub.py | 316 ++-------- DeDRM_plugin/ineptpdf.py | 440 ++------------ DeDRM_plugin/ion.py | 31 +- DeDRM_plugin/kindlekey.py | 801 +------------------------ DeDRM_plugin/mobidedrm.py | 9 +- DeDRM_plugin/openssl_des.py | 89 --- DeDRM_plugin/pycrypto_des.py | 30 - DeDRM_plugin/python_des.py | 220 ------- Obok_plugin/obok/obok.py | 119 +--- 16 files changed, 202 insertions(+), 2211 deletions(-) delete mode 100644 DeDRM_plugin/openssl_des.py delete mode 100644 DeDRM_plugin/pycrypto_des.py delete mode 100644 DeDRM_plugin/python_des.py diff --git a/DeDRM_plugin/adobekey.py b/DeDRM_plugin/adobekey.py index 6155cff..36c18de 100644 --- a/DeDRM_plugin/adobekey.py +++ b/DeDRM_plugin/adobekey.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# adobekey.pyw, version 7.1 -# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al. +# adobekey.pyw, version 7.4 +# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # @@ -32,6 +32,7 @@ # 7.1 - Fix "failed to decrypt user key key" error (read username from registry) # 7.2 - Fix decryption error on Python2 if there's unicode in the username # 7.3 - Fix OpenSSL in Wine +# 7.4 - Remove OpenSSL support to only support PyCryptodome """ Retrieve Adobe ADEPT user key. @@ -125,91 +126,12 @@ if iswindows: except ImportError: import _winreg as winreg - def get_fake_windows_libcrypto_path(): - # There seems to be a bug in Wine where a `find_library('libcrypto-1_1')` - # will not return the path to the libcrypto-1_1.dll file. - # So if we're on Windows, and we didn't find the libcrypto the normal way, - # lets try a hack-y workaround. It's already over anyways at this - # point, can't really make it worse. - import sys, os - for p in sys.path: - if os.path.isfile(os.path.join(p, "libcrypto-1_1.dll")): - return os.path.join(p, "libcrypto-1_1.dll") - if os.path.isfile(os.path.join(p, "libeay32.dll")): - return os.path.join(p, "libeay32.dll") - return None - - def _load_crypto_libcrypto(): - from ctypes.util import find_library - libcrypto = find_library('libcrypto-1_1') - if libcrypto is None: - libcrypto = find_library('libeay32') - if libcrypto is None: - libcrypto = get_fake_windows_libcrypto_path() - if libcrypto is None: - raise ADEPTError('libcrypto not found') - libcrypto = CDLL(libcrypto) - AES_MAXNR = 14 - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - def decrypt(self, data): - out = create_string_buffer(len(data)) - iv = (b"\x00" * self._blocksize) - rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) - if rv == 0: - raise ADEPTError('AES decryption failed') - return out.raw - return AES - - def _load_crypto_pycrypto(): - try: - from Crypto.Cipher import AES as _AES - except (ImportError, ModuleNotFoundError): - from Cryptodome.Cipher import AES as _AES - class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16) - def decrypt(self, data): - return self._aes.decrypt(data) - return AES - - def _load_crypto(): - AES = None - for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto): - try: - AES = loader() - break - except (ImportError, ModuleNotFoundError, ADEPTError): - pass - return AES - - AES = _load_crypto() - + try: + from Cryptodome.Cipher import AES + from Cryptodome.Util.Padding import unpad + except ImportError: + from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device' PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' @@ -402,8 +324,6 @@ if iswindows: CryptUnprotectData = CryptUnprotectData() def adeptkeys(): - if AES is None: - raise ADEPTError("PyCrypto or OpenSSL must be installed") root = GetSystemDirectory().split('\\')[0] + '\\' serial = GetVolumeSerialNumber(root) vendor = cpuid0() @@ -416,7 +336,7 @@ if iswindows: try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) device = winreg.QueryValueEx(regkey, 'key')[0] - except WindowsError, FileNotFoundError: + except (WindowsError, FileNotFoundError): raise ADEPTError("Adobe Digital Editions not activated") keykey = CryptUnprotectData(device, entropy) userkey = None @@ -424,7 +344,7 @@ if iswindows: names = [] try: plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) - except WindowsError, FileNotFoundError: + except (WindowsError, FileNotFoundError): raise ADEPTError("Could not locate ADE activation") i = -1 @@ -443,7 +363,7 @@ if iswindows: for j in range(0, 16): try: plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) - except WindowsError, FileNotFoundError: + except (WindowsError, FileNotFoundError): break ktype = winreg.QueryValueEx(plkkey, None)[0] if ktype == 'user': @@ -461,10 +381,7 @@ if iswindows: pass if ktype == 'privateLicenseKey': userkey = winreg.QueryValueEx(plkkey, 'value')[0] - userkey = b64decode(userkey) - aes = AES(keykey) - userkey = aes.decrypt(userkey) - userkey = userkey[26:-ord(userkey[-1:])] + userkey = unpad(AES.new(keykey, AES.MODE_CBC, b'\x00'*16).decrypt(b64decode(userkey)), 16)[26:] # print ("found " + uuid_name + " key: " + str(userkey)) keys.append(userkey) diff --git a/DeDRM_plugin/adobekey_get_passhash.py b/DeDRM_plugin/adobekey_get_passhash.py index 05f4c32..72b7cd5 100644 --- a/DeDRM_plugin/adobekey_get_passhash.py +++ b/DeDRM_plugin/adobekey_get_passhash.py @@ -23,20 +23,13 @@ import sys, os, time import base64, hashlib try: from Cryptodome.Cipher import AES -except: + from Cryptodome.Util.Padding import unpad +except ImportError: from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" -def unpad(data): - - if sys.version_info[0] == 2: - pad_len = ord(data[-1]) - else: - pad_len = data[-1] - - return data[:-pad_len] - try: from calibre.constants import iswindows, isosx @@ -55,7 +48,7 @@ def decrypt_passhash(passhash, fp): hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16] encrypted_cc_hash = base64.b64decode(passhash) - cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:])) + cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16) return base64.b64encode(cc_hash).decode("ascii") diff --git a/DeDRM_plugin/androidkindlekey.py b/DeDRM_plugin/androidkindlekey.py index da34c1d..6b152f3 100755 --- a/DeDRM_plugin/androidkindlekey.py +++ b/DeDRM_plugin/androidkindlekey.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # androidkindlekey.py -# Copyright © 2010-20 by Thom, Apprentice Harper et al. +# Copyright © 2010-22 by Thom, Apprentice Harper et al. # Revision history: # 1.0 - AmazonSecureStorage.xml decryption to serial number @@ -14,13 +14,14 @@ # 1.4 - Fix some problems identified by Aldo Bleeker # 1.5 - Fix another problem identified by Aldo Bleeker # 2.0 - Python 3 compatibility +# 2.1 - Remove OpenSSL support; only support PyCryptodome """ Retrieve Kindle for Android Serial Number. """ __license__ = 'GPL v3' -__version__ = '2.0' +__version__ = '2.1' import os import sys @@ -33,6 +34,13 @@ from hashlib import md5 from io import BytesIO from binascii import a2b_hex, b2a_hex +try: + from Cryptodome.Cipher import AES, DES + from Cryptodome.Util.Padding import pad, unpad +except ImportError: + from Crypto.Cipher import AES, DES + from Crypto.Util.Padding import pad, unpad + # Routines common to Mac and PC # Wrap a stream so that output gets flushed immediately @@ -115,24 +123,16 @@ class AndroidObfuscation(object): key = a2b_hex('0176e04c9408b1702d90be333fd53523') + def _get_cipher(self): + return AES.new(self.key, AES.MODE_ECB) + def encrypt(self, plaintext): - cipher = self._get_cipher() - padding = len(self.key) - len(plaintext) % len(self.key) - plaintext += chr(padding) * padding - return b2a_hex(cipher.encrypt(plaintext.encode('utf-8'))) + pt = pad(plaintext.encode('utf-8'), 16) + return b2a_hex(self._get_cipher().encrypt(pt)) def decrypt(self, ciphertext): - cipher = self._get_cipher() - plaintext = cipher.decrypt(a2b_hex(ciphertext)) - return plaintext[:-ord(plaintext[-1])] - - def _get_cipher(self): - try: - from Crypto.Cipher import AES - return AES.new(self.key) - except ImportError: - from aescbc import AES, noPadding - return AES(self.key, padding=noPadding()) + ct = a2b_hex(ciphertext) + return unpad(self._get_cipher().decrypt(ct), 16) class AndroidObfuscationV2(AndroidObfuscation): '''AndroidObfuscationV2 @@ -149,12 +149,7 @@ class AndroidObfuscationV2(AndroidObfuscation): self.iv = key[8:16] def _get_cipher(self): - try : - from Crypto.Cipher import DES - return DES.new(self.key, DES.MODE_CBC, self.iv) - except ImportError: - from python_des import Des, CBC - return Des(self.key, CBC, self.iv) + return DES.new(self.key, DES.MODE_CBC, self.iv) def parse_preference(path): ''' parse android's shared preference xml ''' diff --git a/DeDRM_plugin/erdr2pml.py b/DeDRM_plugin/erdr2pml.py index 26c947d..6a20fdb 100755 --- a/DeDRM_plugin/erdr2pml.py +++ b/DeDRM_plugin/erdr2pml.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # erdr2pml.py -# Copyright © 2008-2021 The Dark Reverser, Apprentice Harper, noDRM et al. +# Copyright © 2008-2022 The Dark Reverser, Apprentice Harper, noDRM et al. # # Changelog # @@ -65,11 +65,17 @@ # 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility # 1.00 - Added Python 3 compatibility for calibre 5.0 # 1.01 - Bugfixes for standalone version. +# 1.02 - Remove OpenSSL support; only use PyCryptodome -__version__='1.00' +__version__='1.02' import sys, re -import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback +import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback, hashlib + +try: + from Cryptodome.Cipher import DES +except ImportError: + from Crypto.Cipher import DES #@@CALIBRE_COMPAT_CODE@@ @@ -136,44 +142,6 @@ def unicode_argv(): argvencoding = sys.stdin.encoding or "utf-8" return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv] -Des = None -if iswindows: - # first try with pycrypto - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - if Des == None: - # they try with openssl - import openssl_des - Des = openssl_des.load_libcrypto() -else: - # first try with openssl - import openssl_des - Des = openssl_des.load_libcrypto() - if Des == None: - # then try with pycrypto - import pycrypto_des - Des = pycrypto_des.load_pycrypto() - -# if that did not work then use pure python implementation -# of DES and try to speed it up with Psycho -if Des == None: - import python_des - Des = python_des.Des - # Import Psyco if available - try: - # http://psyco.sourceforge.net - import psyco - psyco.full() - except ImportError: - pass - -try: - from hashlib import sha1 -except ImportError: - # older Python release - import sha - sha1 = lambda s: sha.new(s) - import cgi import logging @@ -253,7 +221,7 @@ class EreaderProcessor(object): raise ValueError('incorrect eReader version %d (error 1)' % version) data = self.section_reader(1) self.data = data - des = Des(fixKey(data[0:8])) + des = DES.new(fixKey(data[0:8]), DES.MODE_ECB) cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: raise ValueError('incorrect eReader version (error 2)') @@ -317,7 +285,7 @@ class EreaderProcessor(object): if (self.flags & reqd_flags) != reqd_flags: print("Flags: 0x%X" % self.flags) raise ValueError('incompatible eReader file') - des = Des(fixKey(user_key)) + des = DES.new(fixKey(user_key), DES.MODE_ECB) if version == 259: if drm_sub_version != 7: raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) @@ -393,7 +361,7 @@ class EreaderProcessor(object): # return bkinfo def getText(self): - des = Des(fixKey(self.content_key)) + des = DES.new(fixKey(self.content_key), DES.MODE_ECB) r = b'' for i in range(self.num_text_pages): logging.debug('get page %d', i) @@ -406,7 +374,7 @@ class EreaderProcessor(object): sect = self.section_reader(self.first_footnote_page) fnote_ids = deXOR(sect, 0, self.xortable) # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) + des = DES.new(fixKey(self.content_key), DES.MODE_ECB) for i in range(1,self.num_footnote_pages): logging.debug('get footnotepage %d', i) id_len = ord(fnote_ids[2]) @@ -430,7 +398,7 @@ class EreaderProcessor(object): sect = self.section_reader(self.first_sidebar_page) sbar_ids = deXOR(sect, 0, self.xortable) # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated - des = Des(fixKey(self.content_key)) + des = DES.new(fixKey(self.content_key), DES.MODE_ECB) for i in range(1,self.num_sidebar_pages): id_len = ord(sbar_ids[2]) id = sbar_ids[3:3+id_len] diff --git a/DeDRM_plugin/ignoblekeyAndroid.py b/DeDRM_plugin/ignoblekeyAndroid.py index 2b3f0ec..5fcbd6a 100644 --- a/DeDRM_plugin/ignoblekeyAndroid.py +++ b/DeDRM_plugin/ignoblekeyAndroid.py @@ -10,22 +10,16 @@ import os import base64 try: from Cryptodome.Cipher import AES -except: + from Cryptodome.Util.Padding import unpad +except ImportError: from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad import hashlib from lxml import etree PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" -def unpad(data): - - if sys.version_info[0] == 2: - pad_len = ord(data[-1]) - else: - pad_len = data[-1] - - return data[:-pad_len] def dump_keys(path_to_adobe_folder): @@ -53,7 +47,7 @@ def dump_keys(path_to_adobe_folder): for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"): encrypted_cc_hash = base64.b64decode(pass_hash.text) - cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:])) + cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16) hashes.append(base64.b64encode(cc_hash).decode("ascii")) #print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii"))) diff --git a/DeDRM_plugin/ignoblekeyGenPassHash.py b/DeDRM_plugin/ignoblekeyGenPassHash.py index cb6d208..e1cdba0 100644 --- a/DeDRM_plugin/ignoblekeyGenPassHash.py +++ b/DeDRM_plugin/ignoblekeyGenPassHash.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # ignoblekeyGenPassHash.py -# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al. +# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # @@ -31,19 +31,25 @@ # 2.7 - Work if TkInter is missing # 2.8 - Fix bug in stand-alone use (import tkFileDialog) # 3.0 - Added Python 3 compatibility for calibre 5.0 +# 3.1 - Remove OpenSSL support, only PyCryptodome is supported now """ Generate Barnes & Noble EPUB user key from name and credit card number. """ __license__ = 'GPL v3' -__version__ = "3.0" +__version__ = "3.1" import sys import os import hashlib import base64 +try: + from Cryptodome.Cipher import AES +except ImportError: + from Crypto.Cipher import AES + # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. @@ -114,87 +120,6 @@ def unicode_argv(): class IGNOBLEError(Exception): pass -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - - class AES(object): - def __init__(self, userkey, iv): - self._blocksize = len(userkey) - self._iv = iv - key = self._key = AES_KEY() - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES Encrypt key') - - def encrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) - if rv == 0: - raise IGNOBLEError('AES encryption failed') - return out.raw - - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - - class AES(object): - def __init__(self, key, iv): - self._aes = _AES.new(key, _AES.MODE_CBC, iv) - - def encrypt(self, data): - return self._aes.encrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - def normalize_name(name): return ''.join(x for x in name.lower() if x != ' ') @@ -215,8 +140,7 @@ def generate_key(name, ccn): name_sha = hashlib.sha1(name).digest()[:16] ccn_sha = hashlib.sha1(ccn).digest()[:16] both_sha = hashlib.sha1(name + ccn).digest() - aes = AES(ccn_sha, name_sha) - crypt = aes.encrypt(both_sha + (b'\x0c' * 0x0c)) + crypt = AES.new(ccn_sha, AES.MODE_CBC, name_sha).encrypt(both_sha + (b'\x0c' * 0x0c)) userkey = hashlib.sha1(crypt).digest() return base64.b64encode(userkey) @@ -226,11 +150,6 @@ def cli_main(): sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) - if AES is None: - print("%s: This script requires OpenSSL or PyCrypto, which must be installed " \ - "separately. Read the top-of-script comment for details." % \ - (progname,)) - return 1 if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 @@ -316,13 +235,6 @@ def gui_main(): self.status['text'] = "Keyfile successfully generated" root = tkinter.Tk() - if AES is None: - root.withdraw() - tkinter.messagebox.showerror( - "Ignoble EPUB Keyfile Generator", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) diff --git a/DeDRM_plugin/ignoblekeyWindowsStore.py b/DeDRM_plugin/ignoblekeyWindowsStore.py index 919d2e6..f22d64d 100644 --- a/DeDRM_plugin/ignoblekeyWindowsStore.py +++ b/DeDRM_plugin/ignoblekeyWindowsStore.py @@ -15,8 +15,10 @@ import apsw import base64 try: from Cryptodome.Cipher import AES + from Cryptodome.Util.Padding import unpad except: from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad import hashlib from lxml import etree @@ -24,15 +26,6 @@ from lxml import etree NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState" PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" -def unpad(data): - - if sys.version_info[0] == 2: - pad_len = ord(data[-1]) - else: - pad_len = data[-1] - - return data[:-pad_len] - def dump_keys(print_result=False): db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3") @@ -64,7 +57,7 @@ def dump_keys(print_result=False): for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"): encrypted_cc_hash = base64.b64decode(pass_hash.text) - cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:])) + cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16) decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii"))) if print_result: print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii"))) diff --git a/DeDRM_plugin/ineptepub.py b/DeDRM_plugin/ineptepub.py index 7f34357..5fc6d4b 100644 --- a/DeDRM_plugin/ineptepub.py +++ b/DeDRM_plugin/ineptepub.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # ineptepub.py -# Copyright © 2009-2021 by i♥cabbages, Apprentice Harper et al. +# Copyright © 2009-2022 by i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # @@ -31,13 +31,14 @@ # 6.6 - Import tkFileDialog, don't assume something else will import it. # 7.0 - Add Python 3 compatibility for calibre 5.0 # 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script +# 7.2 - Only support PyCryptodome; clean up the code """ Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "7.1" +__version__ = "7.2" import sys import os @@ -49,6 +50,15 @@ from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from contextlib import closing from lxml import etree +try: + from Cryptodome.Cipher import AES, PKCS1_v1_5 + from Cryptodome.PublicKey import RSA + from Cryptodome.Util.Padding import unpad +except ImportError: + from Crypto.Cipher import AES, PKCS1_v1_5 + from Crypto.PublicKey import RSA + from Crypto.Util.Padding import unpad + # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. @@ -120,234 +130,6 @@ class ADEPTError(Exception): class ADEPTNewVersionError(Exception): pass -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ADEPTError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - RSA_NO_PADDING = 3 - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class RSA(Structure): - pass - RSA_p = POINTER(RSA) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', - [RSA_p, c_char_pp, c_long]) - RSA_size = F(c_int, 'RSA_size', [RSA_p]) - RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', - [c_int, c_char_p, c_char_p, RSA_p, c_int]) - RSA_free = F(None, 'RSA_free', [RSA_p]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - - class RSA(object): - def __init__(self, der): - buf = create_string_buffer(der) - pp = c_char_pp(cast(buf, c_char_p)) - rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) - if rsa is None: - raise ADEPTError('Error parsing ADEPT user key DER') - - def decrypt(self, from_): - rsa = self._rsa - to = create_string_buffer(RSA_size(rsa)) - dlen = RSA_private_decrypt(len(from_), from_, to, rsa, - RSA_NO_PADDING) - if dlen < 0: - raise ADEPTError('RSA decryption failed') - return to[:dlen] - - def __del__(self): - if self._rsa is not None: - RSA_free(self._rsa) - self._rsa = None - - class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - - def decrypt(self, data): - out = create_string_buffer(len(data)) - iv = (b"\x00" * self._blocksize) - rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) - if rv == 0: - raise ADEPTError('AES decryption failed') - return out.raw - - return (AES, RSA) - -def _load_crypto_pycrypto(): - try: - from Cryptodome.Cipher import AES as _AES - from Cryptodome.PublicKey import RSA as _RSA - from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 - except: - from Crypto.Cipher import AES as _AES - from Crypto.PublicKey import RSA as _RSA - from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 - - # ASN.1 parsing code from tlslite - class ASN1Error(Exception): - pass - - class ASN1Parser(object): - class Parser(object): - def __init__(self, bytes): - self.bytes = bytes - self.index = 0 - - def get(self, length): - if self.index + length > len(self.bytes): - raise ASN1Error("Error decoding ASN.1") - x = 0 - for count in range(length): - x <<= 8 - x |= self.bytes[self.index] - self.index += 1 - return x - - def getFixBytes(self, lengthBytes): - bytes = self.bytes[self.index : self.index+lengthBytes] - self.index += lengthBytes - return bytes - - def getVarBytes(self, lengthLength): - lengthBytes = self.get(lengthLength) - return self.getFixBytes(lengthBytes) - - def getFixList(self, length, lengthList): - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def getVarList(self, length, lengthLength): - lengthList = self.get(lengthLength) - if lengthList % length != 0: - raise ASN1Error("Error decoding ASN.1") - lengthList = int(lengthList/length) - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def startLengthCheck(self, lengthLength): - self.lengthCheck = self.get(lengthLength) - self.indexCheck = self.index - - def setLengthCheck(self, length): - self.lengthCheck = length - self.indexCheck = self.index - - def stopLengthCheck(self): - if (self.index - self.indexCheck) != self.lengthCheck: - raise ASN1Error("Error decoding ASN.1") - - def atLengthCheck(self): - if (self.index - self.indexCheck) < self.lengthCheck: - return False - elif (self.index - self.indexCheck) == self.lengthCheck: - return True - else: - raise ASN1Error("Error decoding ASN.1") - - def __init__(self, bytes): - p = self.Parser(bytes) - p.get(1) - self.length = self._getASN1Length(p) - self.value = p.getFixBytes(self.length) - - def getChild(self, which): - p = self.Parser(self.value) - for x in range(which+1): - markIndex = p.index - p.get(1) - length = self._getASN1Length(p) - p.getFixBytes(length) - return ASN1Parser(p.bytes[markIndex:p.index]) - - def _getASN1Length(self, p): - firstLength = p.get(1) - if firstLength<=127: - return firstLength - else: - lengthLength = firstLength & 0x7F - return p.get(lengthLength) - - class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16) - - def decrypt(self, data): - return self._aes.decrypt(data) - - class RSA(object): - def __init__(self, der): - key = ASN1Parser([x for x in der]) - key = [key.getChild(x).value for x in range(1, 4)] - key = [self.bytesToNumber(v) for v in key] - self._rsa = _RSA.construct(key) - - def bytesToNumber(self, bytes): - total = 0 - for byte in bytes: - total = (total << 8) + byte - return total - - def decrypt(self, data): - return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172) - - return (AES, RSA) - -def _load_crypto(): - AES = RSA = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES, RSA = loader() - break - except (ImportError, ADEPTError): - pass - return (AES, RSA) - -AES, RSA = _load_crypto() - META_NAMES = ('mimetype', 'META-INF/rights.xml') NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} @@ -355,7 +137,7 @@ NSMAP = {'adept': 'http://ns.adobe.com/adept', class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) + self._aes = AES.new(bookkey, AES.MODE_CBC, b'\x00'*16) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() self._otherData = otherData = set() @@ -373,11 +155,11 @@ class Decryptor(object): path = path.encode('utf-8') encrypted.add(path) json_elements_to_remove.add(elem.getparent().getparent()) - else: + else: path = path.encode('utf-8') otherData.add(path) self._has_remaining_xml = True - + for elem in json_elements_to_remove: elem.getparent().remove(elem) @@ -398,8 +180,8 @@ class Decryptor(object): except: # possibly not compressed by zip - just return bytes return bytes - return decompressed_bytes - + return decompressed_bytes + def decrypt(self, path, data): if path.encode('utf-8') in self._encrypted: data = self._aes.decrypt(data)[16:] @@ -446,49 +228,26 @@ def isPassHashBook(inpath): return True except: pass - + return False -# Checks the license file and returns the UUID the book is licensed for. +# Checks the license file and returns the UUID the book is licensed for. # This is used so that the Calibre plugin can pick the correct decryption key # first try without having to loop through all possible keys. -def adeptGetUserUUID(inpath): +def adeptGetUserUUID(inpath): with closing(ZipFile(open(inpath, 'rb'))) as inf: try: rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('user'),) user_uuid = ''.join(rights.findtext(expr)) - if user_uuid[:9] != "urn:uuid:": + if user_uuid[:9] != "urn:uuid:": return None return user_uuid[9:] except: return None -def verify_book_key(bookkey): - if bookkey[-17] != '\x00' and bookkey[-17] != 0: - # Byte not null, invalid result - return False - - if ((bookkey[0] != '\x02' and bookkey[0] != 2) and - ((bookkey[0] != '\x00' and bookkey[0] != 0) or - (bookkey[1] != '\x02' and bookkey[1] != 2))): - # Key not starting with "00 02" or "02" -> error - return False - - keylen = len(bookkey) - 17 - for i in range(1, keylen): - if bookkey[i] == 0 or bookkey[i] == '\x00': - # Padding data contains a space - that's not allowed. - # Probably bad decryption. - return False - - return True - def decryptBook(userkey, inpath, outpath): - if AES is None: - raise ADEPTError("PyCrypto or OpenSSL must be installed.") - with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = inf.namelist() if 'META-INF/rights.xml' not in namelist or \ @@ -508,7 +267,7 @@ def decryptBook(userkey, inpath, outpath): print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).") print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.") raise ADEPTNewVersionError("Book uses new ADEPT encryption") - + if len(bookkey) == 172: print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath))) elif len(bookkey) == 64: @@ -519,28 +278,21 @@ def decryptBook(userkey, inpath, outpath): if len(bookkey) != 64: # Normal Adobe ADEPT - rsa = RSA(userkey) - bookkey = rsa.decrypt(base64.b64decode(bookkey.encode('ascii'))) + rsakey = RSA.import_key(userkey) # parses the ASN1 structure + bookkey = base64.b64decode(bookkey) + try: + bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads + except ValueError: + bookkey = None - # Verify key: - if len(bookkey) > 16: - # Padded as per RSAES-PKCS1-v1_5 - if verify_book_key(bookkey): - bookkey = bookkey[-16:] - else: - print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) - return 2 - else: + if bookkey is None: + print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) + return 2 + else: # Adobe PassHash / B&N key = base64.b64decode(userkey)[:16] - aes = AES(key) - bookkey = aes.decrypt(base64.b64decode(bookkey)) - if type(bookkey[-1]) != int: - pad = ord(bookkey[-1]) - else: - pad = bookkey[-1] - - bookkey = bookkey[:-pad] + bookkey = base64.b64decode(bookkey) + bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7 if len(bookkey) > 16: bookkey = bookkey[-16:] diff --git a/DeDRM_plugin/ineptpdf.py b/DeDRM_plugin/ineptpdf.py index 9bbf092..2364c12 100755 --- a/DeDRM_plugin/ineptpdf.py +++ b/DeDRM_plugin/ineptpdf.py @@ -3,7 +3,7 @@ # ineptpdf.py # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. -# Copyright © 2021 by noDRM +# Copyright © 2021-2022 by noDRM et al. # Released under the terms of the GNU General Public Licence, version 3 # @@ -48,27 +48,37 @@ # 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog # 9.0.0 - Add Python 3 compatibility for calibre 5 # 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs. +# 9.1.1 - Only support PyCryptodome; clean up the code """ Decrypts Adobe ADEPT-encrypted PDF files. """ __license__ = 'GPL v3' -__version__ = "9.1.0" +__version__ = "9.1.1" import codecs +import hashlib import sys import os import re import zlib import struct -import hashlib from io import BytesIO from decimal import Decimal import itertools import xml.etree.ElementTree as etree import traceback +try: + from Cryptodome.Cipher import AES, ARC4, PKCS1_v1_5 + from Cryptodome.PublicKey import RSA + from Cryptodome.Util.Padding import unpad +except ImportError: + from Crypto.Cipher import AES, ARC4, PKCS1_v1_5 + from Crypto.PublicKey import RSA + from Crypto.Util.Padding import unpad + # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. @@ -140,313 +150,8 @@ class ADEPTInvalidPasswordError(Exception): class ADEPTNewVersionError(Exception): pass - -import hashlib - def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ADEPTError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - RSA_NO_PADDING = 3 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - class RC4_KEY(Structure): - _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] - RC4_KEY_p = POINTER(RC4_KEY) - - class RSA(Structure): - pass - RSA_p = POINTER(RSA) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',[c_char_p, c_int, AES_KEY_p]) - - RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) - RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) - - d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', - [RSA_p, c_char_pp, c_long]) - RSA_size = F(c_int, 'RSA_size', [RSA_p]) - RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', - [c_int, c_char_p, c_char_p, RSA_p, c_int]) - RSA_free = F(None, 'RSA_free', [RSA_p]) - - class RSA(object): - def __init__(self, der): - buf = create_string_buffer(der) - pp = c_char_pp(cast(buf, c_char_p)) - rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) - if rsa is None: - raise ADEPTError('Error parsing ADEPT user key DER') - - def decrypt(self, from_): - rsa = self._rsa - to = create_string_buffer(RSA_size(rsa)) - dlen = RSA_private_decrypt(len(from_), from_, to, rsa, - RSA_NO_PADDING) - if dlen < 0: - raise ADEPTError('RSA decryption failed') - return to[1:dlen] - - def __del__(self): - if self._rsa is not None: - RSA_free(self._rsa) - self._rsa = None - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._blocksize = len(userkey) - key = self._key = RC4_KEY() - RC4_set_key(key, self._blocksize, userkey) - return self - def __init__(self): - self._blocksize = 0 - self._key = None - def decrypt(self, data): - out = create_string_buffer(len(data)) - RC4_crypt(self._key, len(data), data, out) - return out.raw - - class AES(object): - MODE_CBC = 0 - @classmethod - def new(cls, userkey, mode, iv, decrypt=True): - self = AES() - self._blocksize = len(userkey) - # mode is ignored since CBCMODE is only thing supported/used so far - self._mode = mode - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ADEPTError('AES improper key used') - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - self._isDecrypt = decrypt - if decrypt: - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - else: - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise ADEPTError('Failed to initialize AES key') - return self - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - self._mode = 0 - self._isDecrypt = None - def decrypt(self, data): - if not self._isDecrypt: - raise ADEPTError("AES not ready for decryption") - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) - if rv == 0: - raise ADEPTError('AES decryption failed') - return out.raw - def encrypt(self, data): - if self._isDecrypt: - raise ADEPTError("AES not ready for encryption") - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 1) - if rv == 0: - raise ADEPTError('AES decryption failed') - return out.raw - - return (ARC4, RSA, AES) - - -def _load_crypto_pycrypto(): - from Crypto.PublicKey import RSA as _RSA - from Crypto.Cipher import ARC4 as _ARC4 - from Crypto.Cipher import AES as _AES - from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 - - # ASN.1 parsing code from tlslite - class ASN1Error(Exception): - pass - - class ASN1Parser(object): - class Parser(object): - def __init__(self, bytes): - self.bytes = bytes - self.index = 0 - - def get(self, length): - if self.index + length > len(self.bytes): - raise ASN1Error("Error decoding ASN.1") - x = 0 - for count in range(length): - x <<= 8 - x |= self.bytes[self.index] - self.index += 1 - return x - - def getFixBytes(self, lengthBytes): - bytes = self.bytes[self.index : self.index+lengthBytes] - self.index += lengthBytes - return bytes - - def getVarBytes(self, lengthLength): - lengthBytes = self.get(lengthLength) - return self.getFixBytes(lengthBytes) - - def getFixList(self, length, lengthList): - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def getVarList(self, length, lengthLength): - lengthList = self.get(lengthLength) - if lengthList % length != 0: - raise ASN1Error("Error decoding ASN.1") - lengthList = int(lengthList/length) - l = [0] * lengthList - for x in range(lengthList): - l[x] = self.get(length) - return l - - def startLengthCheck(self, lengthLength): - self.lengthCheck = self.get(lengthLength) - self.indexCheck = self.index - - def setLengthCheck(self, length): - self.lengthCheck = length - self.indexCheck = self.index - - def stopLengthCheck(self): - if (self.index - self.indexCheck) != self.lengthCheck: - raise ASN1Error("Error decoding ASN.1") - - def atLengthCheck(self): - if (self.index - self.indexCheck) < self.lengthCheck: - return False - elif (self.index - self.indexCheck) == self.lengthCheck: - return True - else: - raise ASN1Error("Error decoding ASN.1") - - def __init__(self, bytes): - p = self.Parser(bytes) - p.get(1) - self.length = self._getASN1Length(p) - self.value = p.getFixBytes(self.length) - - def getChild(self, which): - p = self.Parser(self.value) - for x in range(which+1): - markIndex = p.index - p.get(1) - length = self._getASN1Length(p) - p.getFixBytes(length) - return ASN1Parser(p.bytes[markIndex:p.index]) - - def _getASN1Length(self, p): - firstLength = p.get(1) - if firstLength<=127: - return firstLength - else: - lengthLength = firstLength & 0x7F - return p.get(lengthLength) - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._arc4 = _ARC4.new(userkey) - return self - def __init__(self): - self._arc4 = None - def decrypt(self, data): - return self._arc4.decrypt(data) - - class AES(object): - MODE_CBC = _AES.MODE_CBC - @classmethod - def new(cls, userkey, mode, iv, decrypt=True): - self = AES() - self._aes = _AES.new(userkey, mode, iv) - self._decrypt = decrypt - return self - def __init__(self): - self._aes = None - self._decrypt = None - def decrypt(self, data): - if not self._decrypt: - raise ADEPTError("AES not ready for decrypt.") - - return self._aes.decrypt(data) - def encrypt(self, data): - if self._decrypt: - raise ADEPTError("AES not ready for encrypt.") - return self._aes.encrypt(data) - - class RSA(object): - def __init__(self, der): - key = ASN1Parser([x for x in der]) - key = [key.getChild(x).value for x in range(1, 4)] - key = [self.bytesToNumber(v) for v in key] - self._rsa = _RSA.construct(key) - - def bytesToNumber(self, bytes): - total = 0 - for byte in bytes: - total = (total << 8) + byte - return total - - def decrypt(self, data): - return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172) - - return (ARC4, RSA, AES) - -def _load_crypto(): - ARC4 = RSA = AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - ARC4, RSA, AES = loader() - break - except (ImportError, ADEPTError): - pass - return (ARC4, RSA, AES) -ARC4, RSA, AES = _load_crypto() - - - + return hashlib.sha256(message).digest() # Do we generate cross reference streams on output? # 0 = never @@ -668,7 +373,7 @@ class PSBaseParser(object): if isinstance(s[j], str): # Python 2 c = s[j] - else: + else: # Python 3 c = bytes([s[j]]) self.tokenstart = self.bufpos+j @@ -1593,14 +1298,14 @@ class PDFDocument(object): def check_user_password(self, password, docid, param): V = int_value(param.get('V', 0)) - if V < 5: + if V < 5: return self.check_user_password_V4(password, docid, param) else: return self.check_user_password_V5(password, param) def check_owner_password(self, password, docid, param): V = int_value(param.get('V', 0)) - if V < 5: + if V < 5: return self.check_owner_password_V4(password, docid, param) else: return self.check_owner_password_V5(password, param) @@ -1664,7 +1369,7 @@ class PDFDocument(object): return plaintext else: aes = AES.new(key, AES.MODE_CBC, iv, False) - new_data = bytes(data * repetitions) + new_data = bytes(data * repetitions) crypt = aes.encrypt(new_data) return crypt @@ -1674,7 +1379,7 @@ class PDFDocument(object): K = SHA256(password + salt + userdata) if R < 6: return K - elif R == 6: + elif R == 6: round_number = 0 done = False while (not done): @@ -1684,24 +1389,7 @@ class PDFDocument(object): raise Exception("K1 < 32 ...") #def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None): E = self.process_with_aes(K[:16], True, K1, 64, K[16:32]) - - E_mod_3 = 0 - for i in range(16): - E_mod_3 += E[i] - E_mod_3 = E_mod_3 % 3 - - if E_mod_3 == 0: - ctx = hashlib.sha256() - ctx.update(E) - K = ctx.digest() - elif E_mod_3 == 1: - ctx = hashlib.sha384() - ctx.update(E) - K = ctx.digest() - else: - ctx = hashlib.sha512() - ctx.update(E) - K = ctx.digest() + K = (hashlib.sha256, hashlib.sha384, hashlib.sha512)[sum(E) % 3](E).digest() if round_number >= 64: ch = int.from_bytes(E[-1:], "big", signed=False) @@ -1720,7 +1408,7 @@ class PDFDocument(object): V = int_value(param.get('V', 0)) if V >= 5: raise Exception("compute_O_rc4_key not possible with V>= 5") - + R = int_value(param.get('R', 0)) length = int_value(param.get('Length', 40)) # Key length (bits) @@ -1730,10 +1418,10 @@ class PDFDocument(object): for _ in range(50): hash = hashlib.md5(hash.digest()[:length//8]) hash = hash.digest()[:length//8] - + # "hash" is the return value of compute_O_rc4_key - Odata = str_value(param.get('O')) + Odata = str_value(param.get('O')) # now call iterate_rc4 ... x = ARC4.new(hash).decrypt(Odata) # 4 if R >= 3: @@ -1741,25 +1429,25 @@ class PDFDocument(object): k = b''.join(bytes([c ^ i]) for c in hash ) x = ARC4.new(k).decrypt(x) - + # "x" is now the padded user password. - # If we wanted to recover / extract the user password, + # If we wanted to recover / extract the user password, # we'd need to trim off the padding string from the end. - # As we just want to get access to the encryption key, + # As we just want to get access to the encryption key, # we can just hand the password into the check_user_password # as it is, as that function would be adding padding anyways. # This trick only works with V4 and lower. - + enc_key = self.check_user_password(x, docid, param) if enc_key is not None: return enc_key return False - - + + def check_user_password_V4(self, password, docid, param): V = int_value(param.get('V', 0)) @@ -1803,10 +1491,10 @@ class PDFDocument(object): is_authenticated = (u1 == U) else: is_authenticated = (u1[:16] == U[:16]) - + if is_authenticated: return key - + return None def initialize_standard(self, password, docid, param): @@ -1842,7 +1530,7 @@ class PDFDocument(object): self.decrypt_key = retval if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False: - raise ADEPTInvalidPasswordError("Password invalid.") + raise ADEPTInvalidPasswordError("Password invalid.") P = int_value(param['P']) @@ -1868,8 +1556,8 @@ class PDFDocument(object): set_decipher = False if V >= 4: - # Check if we need new genkey_v4 - only if we're using AES. - try: + # Check if we need new genkey_v4 - only if we're using AES. + try: for key in param['CF']: algo = str(param["CF"][key]["CFM"]) if algo == "/AESV2": @@ -1893,46 +1581,26 @@ class PDFDocument(object): self.decipher = self.decrypt_rc4 # XXX may be AES # aes if not set_decipher: - # This should usually already be set by now. + # This should usually already be set by now. # If it's not, assume that V4 and newer are using AES if V >= 4: self.decipher = self.decrypt_aes self.ready = True return - def verify_book_key(self, bookkey): - if bookkey[-17] != '\x00' and bookkey[-17] != 0: - # Byte not null, invalid result - return False - - if ((bookkey[0] != '\x02' and bookkey[0] != 2) and - ((bookkey[0] != '\x00' and bookkey[0] != 0) or - (bookkey[1] != '\x02' and bookkey[1] != 2))): - # Key not starting with "00 02" or "02" -> error - return False - - keylen = len(bookkey) - 17 - for i in range(1, keylen): - if bookkey[i] == 0 or bookkey[i] == '\x00': - # Padding data contains a space - that's not allowed. - # Probably bad decryption. - return False - - return True def initialize_ebx_ignoble(self, keyb64, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True key = keyb64.decode('base64')[:16] - aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key)) length = int_value(param.get('Length', 0)) / 8 rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = zlib.decompress(rights, -15) rights = etree.fromstring(rights) expr = './/{http://ns.adobe.com/adept}encryptedKey' bookkey = ''.join(rights.findtext(expr)).decode('base64') - bookkey = aes.decrypt(bookkey) - bookkey = bookkey[:-ord(bookkey[-1])] - bookkey = bookkey[-16:] + bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7 + if len(bookkey) > 16: + bookkey = bookkey[-16:] ebx_V = int_value(param.get('V', 4)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) # added because of improper booktype / decryption book session key errors @@ -1967,7 +1635,7 @@ class PDFDocument(object): def initialize_ebx_inept(self, password, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True - rsa = RSA(password) + rsakey = RSA.import_key(password) # parses the ASN1 structure length = int_value(param.get('Length', 0)) // 8 rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64') rights = zlib.decompress(rights, -15) @@ -1983,14 +1651,13 @@ class PDFDocument(object): raise ADEPTNewVersionError("Book uses new ADEPT encryption") bookkey = codecs.decode(bookkey.encode('utf-8'),'base64') - bookkey = rsa.decrypt(bookkey) + try: + bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads + except ValueError: + bookkey = None - if len(bookkey) > 16: - if (self.verify_book_key(bookkey)): - bookkey = bookkey[-16:] - length = 16 - else: - raise ADEPTError('error decrypting book session key') + if bookkey is None: + raise ADEPTError('error decrypting book session key') ebx_V = int_value(param.get('V', 4)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) @@ -2357,7 +2024,7 @@ class PDFObjStrmParser(PDFParser): # Takes a PDF file name as input, and if this is an ADE-protected PDF, # returns the UUID of the user that's licensed to open this file. def adeptGetUserUUID(inf): - try: + try: doc = PDFDocument() inf = open(inf, 'rb') pars = PDFParser(doc, inf) @@ -2376,11 +2043,11 @@ def adeptGetUserUUID(inf): rights = etree.fromstring(rights) expr = './/{http://ns.adobe.com/adept}user' user_uuid = ''.join(rights.findtext(expr)) - if user_uuid[:9] != "urn:uuid:": + if user_uuid[:9] != "urn:uuid:": return None return user_uuid[9:] - except: + except: return None ### @@ -2585,8 +2252,6 @@ class PDFSerializer(object): def decryptBook(userkey, inpath, outpath, inept=True): - if RSA is None: - raise ADEPTError("PyCryptodome or OpenSSL must be installed.") with open(inpath, 'rb') as inf: serializer = PDFSerializer(inf, userkey, inept) with open(outpath, 'wb') as outf: @@ -2601,8 +2266,6 @@ def decryptBook(userkey, inpath, outpath, inept=True): def getPDFencryptionType(inpath): - if RSA is None: - raise ADEPTError("PyCryptodome or OpenSSL must be installed.") with open(inpath, 'rb') as inf: doc = doc = PDFDocument() parser = PDFParser(doc, inf) @@ -2735,13 +2398,6 @@ def gui_main(): root = tkinter.Tk() - if RSA is None: - root.withdraw() - tkinter.messagebox.showerror( - "INEPT PDF", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 root.title("Adobe Adept PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) diff --git a/DeDRM_plugin/ion.py b/DeDRM_plugin/ion.py index f102ec5..f9cd879 100644 --- a/DeDRM_plugin/ion.py +++ b/DeDRM_plugin/ion.py @@ -30,8 +30,14 @@ import struct from io import BytesIO -from Crypto.Cipher import AES -from Crypto.Util.py3compat import bchr +try: + from Cryptodome.Cipher import AES + from Cryptodome.Util.py3compat import bchr + from Cryptodome.Util.Padding import unpad +except ImportError: + from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad + from Crypto.Util.py3compat import bchr try: # lzma library from calibre 4.6.0 or later @@ -744,23 +750,6 @@ def addprottable(ion): ion.addtocatalog("ProtectedData", 1, SYM_NAMES) -def pkcs7pad(msg, blocklen): - paddinglen = blocklen - len(msg) % blocklen - padding = bchr(paddinglen) * paddinglen - return msg + padding - - -def pkcs7unpad(msg, blocklen): - _assert(len(msg) % blocklen == 0) - - paddinglen = msg[-1] - - _assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key") - _assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key") - - return msg[:-paddinglen] - - # every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret OBFUSCATION_TABLE = { "V1": (0x00, None), @@ -876,7 +865,7 @@ class DrmIonVoucher(object): key = hmac.new(sharedsecret, b"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) + b = unpad(b, 16) self.drmkey = BinaryIonParser(BytesIO(b)) addprottable(self.drmkey) @@ -1082,7 +1071,7 @@ class DrmIon(object): def processpage(self, ct, civ, outpages, decompress, decrypt): if decrypt: aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) - msg = pkcs7unpad(aes.decrypt(ct), 16) + msg = unpad(aes.decrypt(ct), 16) else: msg = ct diff --git a/DeDRM_plugin/kindlekey.py b/DeDRM_plugin/kindlekey.py index f9f79a4..0ce800d 100644 --- a/DeDRM_plugin/kindlekey.py +++ b/DeDRM_plugin/kindlekey.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- # kindlekey.py -# Copyright © 2008-2020 Apprentice Harper et al. +# Copyright © 2008-2022 Apprentice Harper et al. __license__ = 'GPL v3' -__version__ = '3.0' +__version__ = '3.1' # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. @@ -30,6 +30,7 @@ __version__ = '3.0' # 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya # 2.8 - Fix for Mac OS X Big Sur # 3.0 - Python 3 for calibre 5.0 +# 3.1 - Only support PyCryptodome; clean up the code """ @@ -42,6 +43,16 @@ from struct import pack, unpack, unpack_from import json import getopt import traceback +import hashlib + +try: + from Cryptodome.Cipher import AES + from Cryptodome.Util import Counter + from Cryptodome.Protocol.KDF import PBKDF2 +except ImportError: + from Crypto.Cipher import AES + from Crypto.Util import Counter + from Crypto.Protocol.KDF import PBKDF2 try: RegError @@ -121,22 +132,16 @@ class DrmException(Exception): pass # crypto digestroutines -import hashlib def MD5(message): - ctx = hashlib.md5() - ctx.update(message) - return ctx.digest() + return hashlib.md5(message).digest() def SHA1(message): - ctx = hashlib.sha1() - ctx.update(message) - return ctx.digest() + return hashlib.sha1(message).digest() def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() + return hashlib.sha256(message).digest() + # For K4M/PC 1.6.X and later def primes(n): @@ -189,6 +194,12 @@ def decode(data,map): result += pack('B',value) return result +def UnprotectHeaderData(encryptedData): + passwdData = b'header_key_data' + salt = b'HEADER.2011' + key_iv = PBKDF2(passwdData, salt, dkLen=256, count=128) + return AES.new(key_iv[0:32], AES.MODE_CBC, key_iv[32:48]).decrypt(encryptedData) + # Routines unique to Mac and PC if iswindows: from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ @@ -205,636 +216,6 @@ if iswindows: advapi32 = windll.advapi32 crypt32 = windll.crypt32 - try: - # try to get fast routines from alfcrypto - from alfcrypto import AES_CBC, KeyIVGen - except: - # alfcrypto not available, so use python implementations - """ - Routines for doing AES CBC in one file - - Modified by some_updates to extract - and combine only those parts needed for AES CBC - into one simple to add python file - - Original Version - Copyright (c) 2002 by Paul A. Lambert - Under: - 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. - """ - - class CryptoError(Exception): - """ Base class for crypto exceptions """ - def __init__(self,errorMessage='Error!'): - self.message = errorMessage - def __str__(self): - return self.message - - class InitCryptoError(CryptoError): - """ Crypto errors during algorithm initialization """ - class BadKeySizeError(InitCryptoError): - """ Bad key size error """ - class EncryptError(CryptoError): - """ Error in encryption processing """ - class DecryptError(CryptoError): - """ Error in decryption processing """ - class DecryptNotBlockAlignedError(DecryptError): - """ Error in decryption processing """ - - def xor(a,b): - """ XOR two byte arrays, to lesser length """ - x = [] - for i in range(min(len(a),len(b))): - x.append( a[i] ^ b[i]) - return bytes(x) - - """ - Base 'BlockCipher' and Pad classes for cipher instances. - BlockCipher supports automatic padding and type conversion. The BlockCipher - class was written to make the actual algorithm code more readable and - not for performance. - """ - - class BlockCipher: - """ Block ciphers """ - def __init__(self): - self.reset() - - def reset(self): - self.resetEncrypt() - self.resetDecrypt() - def resetEncrypt(self): - self.encryptBlockCount = 0 - self.bytesToEncrypt = b'' - def resetDecrypt(self): - self.decryptBlockCount = 0 - self.bytesToDecrypt = b'' - - def encrypt(self, plainText, more = None): - """ Encrypt a string and return a binary string """ - self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt - numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize) - cipherText = '' - for i in range(numBlocks): - bStart = i*self.blockSize - ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize]) - self.encryptBlockCount += 1 - cipherText += ctBlock - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # no more data expected from caller - finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) - if len(finalBytes) > 0: - ctBlock = self.encryptBlock(finalBytes) - self.encryptBlockCount += 1 - cipherText += ctBlock - self.resetEncrypt() - return cipherText - - def decrypt(self, cipherText, more = None): - """ Decrypt a string and return a string """ - self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt - - numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) - if more == None: # no more calls to decrypt, should have all the data - if numExtraBytes != 0: - raise DecryptNotBlockAlignedError('Data not block aligned on decrypt') - - # hold back some bytes in case last decrypt has zero len - if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : - numBlocks -= 1 - numExtraBytes = self.blockSize - - plainText = b'' - for i in range(numBlocks): - bStart = i*self.blockSize - ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) - self.decryptBlockCount += 1 - plainText += ptBlock - - if numExtraBytes > 0: # save any bytes that are not block aligned - self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] - else: - self.bytesToEncrypt = '' - - if more == None: # last decrypt remove padding - plainText = self.padding.removePad(plainText, self.blockSize) - self.resetDecrypt() - return plainText - - - class Pad: - def __init__(self): - pass # eventually could put in calculation of min and max size extension - - class padWithPadLen(Pad): - """ Pad a binary string with the length of the padding """ - - def addPad(self, extraBytes, blockSize): - """ Add padding to a binary string to make it an even multiple - of the block size """ - blocks, numExtraBytes = divmod(len(extraBytes), blockSize) - padLength = blockSize - numExtraBytes - return extraBytes + padLength*chr(padLength) - - def removePad(self, paddedBinaryString, blockSize): - """ Remove padding from a binary string """ - if not(0 6 and i%Nk == 4 : - temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) - w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) - return w - - Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! - 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, - 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) - - #------------------------------------- - def AddRoundKey(algInstance, keyBlock): - """ XOR the algorithm state with a block of key material """ - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] ^= keyBlock[column][row] - #------------------------------------- - - def SubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = Sbox[algInstance.state[column][row]] - - def InvSubBytes(algInstance): - for column in range(algInstance.Nb): - for row in range(4): - algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] - - Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, - 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, - 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, - 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, - 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, - 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, - 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, - 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, - 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, - 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, - 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, - 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, - 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, - 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, - 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, - 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, - 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, - 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, - 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, - 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, - 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, - 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, - 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, - 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, - 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, - 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, - 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, - 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, - 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, - 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, - 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, - 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) - - InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, - 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, - 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, - 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, - 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, - 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, - 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, - 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, - 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, - 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, - 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, - 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, - 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, - 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, - 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, - 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, - 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, - 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, - 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, - 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, - 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, - 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, - 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, - 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, - 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, - 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, - 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, - 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, - 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, - 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, - 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, - 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) - - #------------------------------------- - """ For each block size (Nb), the ShiftRow operation shifts row i - by the amount Ci. Note that row 0 is not shifted. - Nb C1 C2 C3 - ------------------- """ - shiftOffset = { 4 : ( 0, 1, 2, 3), - 5 : ( 0, 1, 2, 3), - 6 : ( 0, 1, 2, 3), - 7 : ( 0, 1, 2, 4), - 8 : ( 0, 1, 3, 4) } - def ShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] - def InvShiftRows(algInstance): - tmp = [0]*algInstance.Nb # list of size Nb - for r in range(1,4): # row 0 reamains unchanged and can be skipped - for c in range(algInstance.Nb): - tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] - for c in range(algInstance.Nb): - algInstance.state[c][r] = tmp[c] - #------------------------------------- - def MixColumns(a): - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) - Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) - Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - - def InvMixColumns(a): - """ Mix the four bytes of every column in a linear way - This is the opposite operation of Mixcolumn """ - Sprime = [0,0,0,0] - for j in range(a.Nb): # for each column - Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) - Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) - Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) - Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) - for i in range(4): - a.state[j][i] = Sprime[i] - - #------------------------------------- - def mul(a, b): - """ Multiply two elements of GF(2^m) - needed for MixColumn and InvMixColumn """ - if (a !=0 and b!=0): - return Alogtable[(Logtable[a] + Logtable[b])%255] - else: - return 0 - - Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, - 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, - 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, - 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, - 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, - 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, - 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, - 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, - 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, - 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, - 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, - 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, - 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, - 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, - 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, - 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) - - Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, - 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, - 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, - 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, - 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, - 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, - 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, - 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, - 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, - 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, - 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, - 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, - 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, - 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, - 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, - 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) - - - - - """ - AES Encryption Algorithm - The AES algorithm is just Rijndael algorithm restricted to the default - blockSize of 128 bits. - """ - - class AES(Rijndael): - """ The AES algorithm is the Rijndael block cipher restricted to block - sizes of 128 bits and key sizes of 128, 192 or 256 bits - """ - def __init__(self, key = None, padding = padWithPadLen(), keySize=16): - """ Initialize AES, keySize is in bytes """ - if not (keySize == 16 or keySize == 24 or keySize == 32) : - raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes') - - Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) - - self.name = 'AES' - - - """ - CBC mode of encryption for block ciphers. - This algorithm mode wraps any BlockCipher to make a - Cipher Block Chaining mode. - """ - from random import Random # should change to crypto.random!!! - - - class CBC(BlockCipher): - """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode - algorithms. The initialization (IV) is automatic if set to None. Padding - is also automatic based on the Pad class used to initialize the algorithm - """ - def __init__(self, blockCipherInstance, padding = padWithPadLen()): - """ CBC algorithms are created by initializing with a BlockCipher instance """ - self.baseCipher = blockCipherInstance - self.name = self.baseCipher.name + '_CBC' - self.blockSize = self.baseCipher.blockSize - self.keySize = self.baseCipher.keySize - self.padding = padding - self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! - self.r = Random() # for IV generation, currently uses - # mediocre standard distro version <---------------- - import time - newSeed = time.ctime()+str(self.r) # seed with instance location - self.r.seed(newSeed) # to make unique - self.reset() - - def setKey(self, key): - self.baseCipher.setKey(key) - - # Overload to reset both CBC state and the wrapped baseCipher - def resetEncrypt(self): - BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) - self.baseCipher.resetEncrypt() # reset base cipher encrypt state - - def resetDecrypt(self): - BlockCipher.resetDecrypt(self) # reset CBC state (super class) - self.baseCipher.resetDecrypt() # reset base cipher decrypt state - - def encrypt(self, plainText, iv=None, more=None): - """ CBC encryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.encryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to encrypt' - - return BlockCipher.encrypt(self,plainText, more=more) - - def decrypt(self, cipherText, iv=None, more=None): - """ CBC decryption - overloads baseCipher to allow optional explicit IV - when iv=None, iv is auto generated! - """ - if self.decryptBlockCount == 0: - self.iv = iv - else: - assert(iv==None), 'IV used only on first call to decrypt' - - return BlockCipher.decrypt(self, cipherText, more=more) - - def encryptBlock(self, plainTextBlock): - """ CBC block encryption, IV is set with 'encrypt' """ - auto_IV = '' - if self.encryptBlockCount == 0: - if self.iv == None: - # generate IV and use - self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) - self.prior_encr_CT_block = self.iv - auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic - else: # application provided IV - assert(len(self.iv) == self.blockSize ),'IV must be same length as block' - self.prior_encr_CT_block = self.iv - """ encrypt the prior CT XORed with the PT """ - ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) - self.prior_encr_CT_block = ct - return auto_IV+ct - - def decryptBlock(self, encryptedBlock): - """ Decrypt a single block """ - - if self.decryptBlockCount == 0: # first call, process IV - if self.iv == None: # auto decrypt IV? - self.prior_CT_block = encryptedBlock - return b'' - else: - assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" - self.prior_CT_block = self.iv - - dct = self.baseCipher.decryptBlock(encryptedBlock) - """ XOR the prior decrypted CT with the prior CT """ - dct_XOR_priorCT = xor( self.prior_CT_block, dct ) - - self.prior_CT_block = encryptedBlock - - return dct_XOR_priorCT - - - """ - AES_CBC Encryption Algorithm - """ - - class aescbc_AES_CBC(CBC): - """ AES encryption in CBC feedback mode """ - def __init__(self, key=None, padding=padWithPadLen(), keySize=16): - CBC.__init__( self, AES(key, noPadding(), keySize), padding) - self.name = 'AES_CBC' - - class AES_CBC(object): - def __init__(self): - self._key = None - self._iv = None - self.aes = None - - def set_decrypt_key(self, userkey, iv): - self._key = userkey - self._iv = iv - self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey)) - - def decrypt(self, data): - iv = self._iv - cleartext = self.aes.decrypt(iv + data) - return cleartext - - import hmac - - class KeyIVGen(object): - # this only exists in openssl so we will use pure python implementation instead - # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - def pbkdf2(self, passwd, salt, iter, keylen): - - def xorbytes( a, b ): - if len(a) != len(b): - raise Exception("xorbytes(): lengths differ") - return bytes([x ^ y for x, y in zip(a, b)]) - - def prf( h, data ): - hm = h.copy() - hm.update( data ) - return hm.digest() - - def pbkdf2_F( h, salt, itercount, blocknum ): - U = prf( h, salt + pack('>i',blocknum ) ) - T = U - for i in range(2, itercount+1): - U = prf( h, U ) - T = xorbytes( T, U ) - return T - - sha = hashlib.sha1 - digest_size = sha().digest_size - # l - number of output blocks to produce - l = keylen // digest_size - if keylen % digest_size != 0: - l += 1 - h = hmac.new( passwd, None, sha ) - T = b"" - for i in range(1, l+1): - T += pbkdf2_F( h, salt, iter, i ) - return T[0: keylen] - - def UnprotectHeaderData(encryptedData): - passwdData = b'header_key_data' - salt = b'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 - # Various character maps used to decrypt kindle info values. # Probably supposed to act as obfuscation charMap2 = b"AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" @@ -1080,7 +461,7 @@ if iswindows: salt = str(0x6d8 * int(build)).encode('utf-8') + guid sp = GetUserName() + b'+@#$%+' + GetIDString().encode('utf-8') passwd = encode(SHA256(sp), charMap5) - key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow + key = PBKDF2(passwd, salt, count=10000, dkLen=0x400)[:32] # this is very slow # loop through the item records until all are processed while len(items) > 0: @@ -1143,8 +524,6 @@ if iswindows: 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 @@ -1174,114 +553,8 @@ if iswindows: DB = {} return DB elif isosx: - import copy import subprocess - # interface to needed routines in openssl's libcrypto - def _load_crypto_libcrypto(): - from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, addressof, string_at, cast - from ctypes.util import find_library - - libcrypto = find_library('crypto') - if libcrypto is None: - libcrypto = '/usr/lib/libcrypto.dylib' - try: - libcrypto = CDLL(libcrypto) - except Exception as e: - raise DrmException("libcrypto not found: " % e) - - # From OpenSSL's crypto aes header - # - # AES_ENCRYPT 1 - # AES_DECRYPT 0 - # AES_MAXNR 14 (in bytes) - # AES_BLOCK_SIZE 16 (in bytes) - # - # struct aes_key_st { - # unsigned long rd_key[4 *(AES_MAXNR + 1)]; - # int rounds; - # }; - # typedef struct aes_key_st AES_KEY; - # - # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); - # - # note: the ivec string, and output buffer are both mutable - # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, - # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc); - - AES_MAXNR = 14 - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int]) - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p]) - - # From OpenSSL's Crypto evp/p5_crpt2.c - # - # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, - # const unsigned char *salt, int saltlen, int iter, - # int keylen, unsigned char *out); - - PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', - [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) - - class LibCrypto(object): - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - - def set_decrypt_key(self, userkey, iv): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise DrmException("AES improper key used") - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - self._userkey = userkey - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise DrmException("Failed to initialize AES key") - - def decrypt(self, data): - out = create_string_buffer(len(data)) - mutable_iv = create_string_buffer(self._iv, len(self._iv)) - keyctx = self._keyctx - rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) - if rv == 0: - raise DrmException("AES decryption failed") - return out.raw - - def keyivgen(self, passwd, salt, iter, keylen): - saltlen = len(salt) - passlen = len(passwd) - out = create_string_buffer(keylen) - rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) - return out.raw - return LibCrypto - - def _load_crypto(): - LibCrypto = None - try: - LibCrypto = _load_crypto_libcrypto() - except (ImportError, DrmException): - pass - return LibCrypto - - LibCrypto = _load_crypto() - # Various character maps used to decrypt books. Probably supposed to act as obfuscation charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' charMap2 = b'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' @@ -1411,33 +684,13 @@ elif isosx: #print "ID Strings:\n",strings return strings - - # unprotect the new header blob in .kinf2011 - # used in Kindle for Mac Version >= 1.9.0 - def UnprotectHeaderData(encryptedData): - passwdData = b'header_key_data' - salt = b'HEADER.2011' - iter = 0x80 - keylen = 0x100 - crp = LibCrypto() - key_iv = crp.keyivgen(passwdData, salt, iter, keylen) - key = key_iv[0:32] - iv = key_iv[32:48] - crp.set_decrypt_key(key,iv) - cleartext = crp.decrypt(encryptedData) - return cleartext - - # implements an Pseudo Mac Version of Windows built-in Crypto routine class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + b'+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) salt = entropy - self.crp = LibCrypto() - iter = 0x800 - keylen = 0x400 - key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) + key_iv = PBKDF2(passwdData, salt, count=0x800, dkLen=0x400) self.key = key_iv[0:32] self.iv = key_iv[32:48] self.crp.set_decrypt_key(self.key, self.iv) @@ -1575,7 +828,7 @@ elif isosx: salt = str(0x6d8 * int(build)).encode('utf-8') + guid sp = GetUserName() + b'+@#$%+' + IDString passwd = encode(SHA256(sp), charMap5) - key = LibCrypto().keyivgen(passwd, salt, 10000, 0x400)[:32] + key = PBKDF2(passwd, salt, count=10000, dkLen=0x400)[:32] #print ("salt",salt) #print ("sp",sp) @@ -1647,8 +900,6 @@ elif isosx: 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 diff --git a/DeDRM_plugin/mobidedrm.py b/DeDRM_plugin/mobidedrm.py index 843eebe..c57b884 100755 --- a/DeDRM_plugin/mobidedrm.py +++ b/DeDRM_plugin/mobidedrm.py @@ -7,7 +7,7 @@ from __future__ import print_function __license__ = 'GPL v3' -__version__ = "1.0" +__version__ = "1.1" # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. @@ -74,6 +74,7 @@ __version__ = "1.0" # 0.41 - Fixed potential unicode problem in command line calls # 0.42 - Added GPL v3 licence. updated/removed some print statements # 1.0 - Python 3 compatibility for calibre 5.0 +# 1.1 - Speed Python PC1 implementation up a little bit import sys import os @@ -175,7 +176,7 @@ def PC1(key, src, decryption=True): wkey = [] for i in range(8): wkey.append(key[i*2]<<8 | key[i*2+1]) - dst = b'' + dst = bytearray(len(src)) for i in range(len(src)): temp1 = 0; byteXorVal = 0; @@ -194,8 +195,8 @@ def PC1(key, src, decryption=True): keyXorVal = curByte * 257; for j in range(8): wkey[j] ^= keyXorVal; - dst+=bytes([curByte]) - return dst + dst[i] = curByte + return bytes(dst) # accepts unicode returns unicode def checksumPid(s): diff --git a/DeDRM_plugin/openssl_des.py b/DeDRM_plugin/openssl_des.py deleted file mode 100644 index 9e455b4..0000000 --- a/DeDRM_plugin/openssl_des.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -# implement just enough of des from openssl to make erdr2pml.py happy - -def load_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - import sys - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - return None - - libcrypto = CDLL(libcrypto) - - # typedef struct DES_ks - # { - # union - # { - # DES_cblock cblock; - # /* make sure things are correct size on machines with - # * 8 byte longs */ - # DES_LONG deslong[2]; - # } ks[16]; - # } DES_key_schedule; - - # just create a big enough place to hold everything - # it will have alignment of structure so we should be okay (16 byte aligned?) - class DES_KEY_SCHEDULE(Structure): - _fields_ = [('DES_cblock1', c_char * 16), - ('DES_cblock2', c_char * 16), - ('DES_cblock3', c_char * 16), - ('DES_cblock4', c_char * 16), - ('DES_cblock5', c_char * 16), - ('DES_cblock6', c_char * 16), - ('DES_cblock7', c_char * 16), - ('DES_cblock8', c_char * 16), - ('DES_cblock9', c_char * 16), - ('DES_cblock10', c_char * 16), - ('DES_cblock11', c_char * 16), - ('DES_cblock12', c_char * 16), - ('DES_cblock13', c_char * 16), - ('DES_cblock14', c_char * 16), - ('DES_cblock15', c_char * 16), - ('DES_cblock16', c_char * 16)] - - DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p]) - DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int]) - - - class DES(object): - def __init__(self, key): - if len(key) != 8 : - raise Exception('DES improper key used') - return - self.key = key - self.keyschedule = DES_KEY_SCHEDULE() - DES_set_key(self.key, self.keyschedule) - def desdecrypt(self, data): - ob = create_string_buffer(len(data)) - DES_ecb_encrypt(data, ob, self.keyschedule, 0) - return ob.raw - def decrypt(self, data): - if not data: - return b'' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return b''.join(result) - - return DES diff --git a/DeDRM_plugin/pycrypto_des.py b/DeDRM_plugin/pycrypto_des.py deleted file mode 100644 index 286df9f..0000000 --- a/DeDRM_plugin/pycrypto_des.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - - -def load_pycrypto(): - try : - from Crypto.Cipher import DES as _DES - except: - return None - - class DES(object): - def __init__(self, key): - if len(key) != 8 : - raise ValueError('DES improper key used') - self.key = key - self._des = _DES.new(key,_DES.MODE_ECB) - def desdecrypt(self, data): - return self._des.decrypt(data) - def decrypt(self, data): - if not data: - return '' - i = 0 - result = [] - while i < len(data): - block = data[i:i+8] - processed_block = self.desdecrypt(block) - result.append(processed_block) - i += 8 - return ''.join(result) - return DES diff --git a/DeDRM_plugin/python_des.py b/DeDRM_plugin/python_des.py deleted file mode 100644 index bd02904..0000000 --- a/DeDRM_plugin/python_des.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab -import sys - -ECB = 0 -CBC = 1 -class Des(object): - __pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, - 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, - 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, - 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3] - __left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] - __pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9, - 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, - 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, - 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31] - __ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, - 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, - 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, - 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6] - __expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, - 7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16, - 15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24, - 23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0] - __sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, - 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, - 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, - 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], - [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, - 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, - 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, - 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], - [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, - 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, - 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, - 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], - [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, - 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, - 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, - 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], - [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, - 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, - 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, - 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], - [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, - 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, - 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, - 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], - [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, - 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, - 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, - 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], - [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, - 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, - 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, - 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],] - __p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25, - 4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24] - __fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30, - 37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28, - 35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26, - 33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24] - # Type of crypting being done - ENCRYPT = 0x00 - DECRYPT = 0x01 - def __init__(self, key, mode=ECB, IV=None): - if len(key) != 8: - raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") - self.block_size = 8 - self.key_size = 8 - self.__padding = '' - self.setMode(mode) - if IV: - self.setIV(IV) - self.L = [] - self.R = [] - self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) - self.final = [] - self.setKey(key) - def getKey(self): - return self.__key - def setKey(self, key): - self.__key = key - self.__create_sub_keys() - def getMode(self): - return self.__mode - def setMode(self, mode): - self.__mode = mode - def getIV(self): - return self.__iv - def setIV(self, IV): - if not IV or len(IV) != self.block_size: - raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") - self.__iv = IV - def getPadding(self): - return self.__padding - def __String_to_BitList(self, data): - l = len(data) * 8 - result = [0] * l - pos = 0 - for c in data: - i = 7 - ch = ord(c) - while i >= 0: - if ch & (1 << i) != 0: - result[pos] = 1 - else: - result[pos] = 0 - pos += 1 - i -= 1 - return result - def __BitList_to_String(self, data): - result = '' - pos = 0 - c = 0 - while pos < len(data): - c += data[pos] << (7 - (pos % 8)) - if (pos % 8) == 7: - result += chr(c) - c = 0 - pos += 1 - return result - def __permutate(self, table, block): - return [block[x] for x in table] - def __create_sub_keys(self): - key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey())) - i = 0 - self.L = key[:28] - self.R = key[28:] - while i < 16: - j = 0 - while j < Des.__left_rotations[i]: - self.L.append(self.L[0]) - del self.L[0] - self.R.append(self.R[0]) - del self.R[0] - j += 1 - self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R) - i += 1 - def __des_crypt(self, block, crypt_type): - block = self.__permutate(Des.__ip, block) - self.L = block[:32] - self.R = block[32:] - if crypt_type == Des.ENCRYPT: - iteration = 0 - iteration_adjustment = 1 - else: - iteration = 15 - iteration_adjustment = -1 - i = 0 - while i < 16: - tempR = self.R[:] - self.R = self.__permutate(Des.__expansion_table, self.R) - self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])] - B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] - j = 0 - Bn = [0] * 32 - pos = 0 - while j < 8: - m = (B[j][0] << 1) + B[j][5] - n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] - v = Des.__sbox[j][(m << 4) + n] - Bn[pos] = (v & 8) >> 3 - Bn[pos + 1] = (v & 4) >> 2 - Bn[pos + 2] = (v & 2) >> 1 - Bn[pos + 3] = v & 1 - pos += 4 - j += 1 - self.R = self.__permutate(Des.__p, Bn) - self.R = [x ^ y for x, y in zip(self.R, self.L)] - self.L = tempR - i += 1 - iteration += iteration_adjustment - self.final = self.__permutate(Des.__fp, self.R + self.L) - return self.final - def crypt(self, data, crypt_type): - if not data: - return '' - if len(data) % self.block_size != 0: - if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") - if not self.getPadding(): - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") - else: - data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() - if self.getMode() == CBC: - if self.getIV(): - iv = self.__String_to_BitList(self.getIV()) - else: - raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") - i = 0 - dict = {} - result = [] - while i < len(data): - block = self.__String_to_BitList(data[i:i+8]) - if self.getMode() == CBC: - if crypt_type == Des.ENCRYPT: - block = [x ^ y for x, y in zip(block, iv)] - processed_block = self.__des_crypt(block, crypt_type) - if crypt_type == Des.DECRYPT: - processed_block = [x ^ y for x, y in zip(processed_block, iv)] - iv = block - else: - iv = processed_block - else: - processed_block = self.__des_crypt(block, crypt_type) - result.append(self.__BitList_to_String(processed_block)) - i += 8 - if crypt_type == Des.DECRYPT and self.getPadding(): - s = result[-1] - while s[-1] == self.getPadding(): - s = s[:-1] - result[-1] = s - return ''.join(result) - def encrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.ENCRYPT) - def decrypt(self, data, pad=''): - self.__padding = pad - return self.crypt(data, Des.DECRYPT) diff --git a/Obok_plugin/obok/obok.py b/Obok_plugin/obok/obok.py index e80e9ae..403db27 100644 --- a/Obok_plugin/obok/obok.py +++ b/Obok_plugin/obok/obok.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# Version 10.0.1 February 2022 +# Remove OpenSSL support to only support PyCryptodome; clean up the code. +# # Version 10.0.0 November 2021 # Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix # key fetch issues on some machines. @@ -162,7 +165,7 @@ """Manage all Kobo books, either encrypted or DRM-free.""" from __future__ import print_function -__version__ = '4.0.0' +__version__ = '10.0.1' __about__ = "Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__) import sys @@ -180,6 +183,13 @@ import shutil import argparse import tempfile +try: + from Cryptodome.Cipher import AES + from Cryptodome.Util.Padding import unpad +except ImportError: + from Crypto.Cipher import AES + from Crypto.Util.Padding import unpad + can_parse_xml = True try: from xml.etree import ElementTree as ET @@ -194,88 +204,6 @@ KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL'] class ENCRYPTIONError(Exception): pass -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise ENCRYPTIONError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_ecb_encrypt = F(None, 'AES_ecb_encrypt', - [c_char_p, c_char_p, AES_KEY_p, c_int]) - - class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise ENCRYPTIONError(_('AES improper key used')) - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise ENCRYPTIONError(_('Failed to initialize AES key')) - - def decrypt(self, data): - clear = b'' - for i in range(0, len(data), 16): - out = create_string_buffer(16) - rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0) - if rv == 0: - raise ENCRYPTIONError(_('AES decryption failed')) - clear += out.raw - return clear - - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_ECB) - - def decrypt(self, data): - return self._aes.decrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, ENCRYPTIONError): - pass - return AES - -AES = _load_crypto() - # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. @@ -630,11 +558,9 @@ class KoboFile(object): file page key. The caller must determine if the decrypted data is correct.""" # The userkey decrypts the page key (self.key) - keyenc = AES(userkey) - decryptedkey = keyenc.decrypt(self.key) - # The decrypted page key decrypts the content - pageenc = AES(decryptedkey) - return self.__removeaespadding(pageenc.decrypt(contents)) + decryptedkey = AES.new(userkey, AES.MODE_ECB).decrypt(self.key) + # The decrypted page key decrypts the content. Padding is PKCS#7 + return unpad(AES.new(decryptedkey, AES.MODE_ECB).decrypt(contents), 16) def check (self, contents): """ @@ -704,23 +630,6 @@ class KoboFile(object): raise ValueError() return False - def __removeaespadding (self, contents): - """ - Remove the trailing padding, using what appears to be the CMS - algorithm from RFC 5652 6.3""" - lastchar = binascii.b2a_hex(contents[-1:]) - strlen = int(lastchar, 16) - padding = strlen - if strlen == 1: - return contents[:-1] - if strlen < 16: - for i in range(strlen): - testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)]) - if testchar != lastchar: - padding = 0 - if padding > 0: - contents = contents[:-padding] - return contents def decrypt_book(book, lib): print("Converting {0}".format(book.title))