diff --git a/Adobe_EPUB_Tools/README_ineptepub.txt b/Adobe_EPUB_Tools/README_ineptepub.txt new file mode 100644 index 0000000..cab043d --- /dev/null +++ b/Adobe_EPUB_Tools/README_ineptepub.txt @@ -0,0 +1,29 @@ +From Apprentice Alf's Blog + +Adobe Adept ePub and PDF, .epub, .pdf + +The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobe’s DRM. Installing these scripts is a little more complex that the Mobipocket and eReader decryption tools, as they require installation of the PyCrypto package for Windows Boxes. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms. + +For more info, see the author's blog: +http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html + +There are two scripts: + +The first is called ineptkey_v5.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information. + +The second is called in ineptepub_v5.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from. + + +Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. + +The latest version of ineptpdf to use is version 8.4.42, which improves support for some PDF files. + +ineptpdf version 8.4.42 can be found here: + +http://pastebin.com/kuKMXXsC + +It is not included in the tools archive. + +If that link is down, please check out the following website for some of the latest releases of these tools: + +http://ainept.freewebspace.com/ diff --git a/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt b/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt new file mode 100644 index 0000000..afcbfe3 --- /dev/null +++ b/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt @@ -0,0 +1,19 @@ +Readme.txt + +Barnes and Noble EPUB ebooks use a form of Social DRM which requires information on your Credit Card Number and the Name on the Credit card used to purchase the book to actually unencrypt the book. + +For more info, see the author's blog: +http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html + +The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X. + +There are 2 scripts: + +The first is ignoblekeygen_v2.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book). + +The second is ignobleepub_v3.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from. + +All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. + +These scripts are based on the IHeartCabbages original scripts that allow the replacement of the requirement for PyCrypto with OpenSSL's libcrypto which is already installed on all Mac OS X machines and Linux Boxes. Window's Users will still have to install PyCrypto or OpenSSL to get these scripts to work properly. + diff --git a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw index c5de3ae..38e23d1 100644 --- a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw +++ b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw @@ -1,18 +1,16 @@ #! /usr/bin/python -# ignobleepub.pyw, version 1-rc2 +# ignobleepub.pyw, version 3 # To run this program install Python 2.6 from -# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto # (make sure to install the version for Python 2.6). Save this script file as # ignobleepub.pyw and double-click on it to run it. # Revision history: # 1 - Initial release - -""" -Decrypt Barnes & Noble ADEPT encrypted EPUB books. -""" +# 2 - Added OS X support by using OpenSSL when available +# 3 - screen out improper key lengths to prevent segfaults on Linux from __future__ import with_statement @@ -30,10 +28,95 @@ import Tkconstants import tkFileDialog import tkMessageBox -try: - from Crypto.Cipher import AES -except ImportError: +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 + + 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_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_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 IGNOBLEError('AES improper key used') + return + key = self._key = AES_KEY() + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) + if rv < 0: + raise IGNOBLEError('Failed to initialize AES key') + + def decrypt(self, data): + out = create_string_buffer(len(data)) + iv = ("\x00" * self._blocksize) + rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) + if rv == 0: + raise IGNOBLEError('AES decryption failed') + return out.raw + + 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_CBC) + + def decrypt(self, data): + return self._aes.decrypt(data) + + return AES + +def _load_crypto(): AES = None + for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto): + try: + AES = loader() + break + except (ImportError, IGNOBLEError): + pass + return AES +AES = _load_crypto() + + + + +""" +Decrypt Barnes & Noble ADEPT encrypted EPUB books. +""" + META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') NSMAP = {'adept': 'http://ns.adobe.com/adept', @@ -49,7 +132,8 @@ class ZipInfo(zipfile.ZipInfo): class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES.new(bookkey, AES.MODE_CBC) + # self._aes = AES.new(bookkey, AES.MODE_CBC) + self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), @@ -75,13 +159,11 @@ class Decryptor(object): return data -class ADEPTError(Exception): - pass def cli_main(argv=sys.argv): progname = os.path.basename(argv[0]) if AES is None: - print "%s: This script requires PyCrypto, which must be installed " \ + print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ "separately. Read the top-of-script comment for details." % \ (progname,) return 1 @@ -92,12 +174,14 @@ def cli_main(argv=sys.argv): with open(keypath, 'rb') as f: keyb64 = f.read() key = keyb64.decode('base64')[:16] - aes = AES.new(key, AES.MODE_CBC) + # aes = AES.new(key, AES.MODE_CBC) + aes = AES(key) + with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: - raise ADEPTError('%s: not an B&N ADEPT EPUB' % (inpath,)) + raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,)) for name in META_NAMES: namelist.remove(name) rights = etree.fromstring(inf.read('META-INF/rights.xml')) @@ -219,7 +303,7 @@ def gui_main(): root.withdraw() tkMessageBox.showerror( "Ignoble EPUB Decrypter", - "This script requires PyCrypto, which must be installed " + "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 root.title('Ignoble EPUB Decrypter') diff --git a/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw index 9d5dc51..70d0535 100644 --- a/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw +++ b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw @@ -1,14 +1,15 @@ #! /usr/bin/python -# ignoblekeygen.pyw, version 1 +# ignoblekeygen.pyw, version 2 # To run this program install Python 2.6 from -# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto # (make sure to install the version for Python 2.6). Save this script file as # ignoblekeygen.pyw and double-click on it to run it. # Revision history: # 1 - Initial release +# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) """ Generate Barnes & Noble EPUB user key from name and credit card number. @@ -26,10 +27,90 @@ import Tkconstants import tkFileDialog import tkMessageBox -try: - from Crypto.Cipher import AES -except ImportError: + + +# use openssl's libcrypt if it exists in place of pycrypto +# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages +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 + + libcrypto = find_library('crypto') + if libcrypto is None: + print 'libcrypto not found' + 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 + for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto): + 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 != ' ') @@ -40,7 +121,7 @@ def generate_keyfile(name, ccn, outpath): name_sha = hashlib.sha1(name).digest()[:16] ccn_sha = hashlib.sha1(ccn).digest()[:16] both_sha = hashlib.sha1(name + ccn).digest() - aes = AES.new(ccn_sha, AES.MODE_CBC, name_sha) + aes = AES(ccn_sha, name_sha) crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) userkey = hashlib.sha1(crypt).digest() with open(outpath, 'wb') as f: @@ -50,7 +131,7 @@ def generate_keyfile(name, ccn, outpath): def cli_main(argv=sys.argv): progname = os.path.basename(argv[0]) if AES is None: - print "%s: This script requires PyCrypto, which must be installed " \ + print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ "separately. Read the top-of-script comment for details." % \ (progname,) return 1 @@ -131,7 +212,7 @@ def gui_main(): root.withdraw() tkMessageBox.showerror( "Ignoble EPUB Keyfile Generator", - "This script requires PyCrypto, which must be installed " + "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 root.title('Ignoble EPUB Keyfile Generator') diff --git a/Calibre_Plugins/K4PCDeDRM_plugin.zip b/Calibre_Plugins/K4PCDeDRM_plugin.zip new file mode 100644 index 0000000..ec61348 Binary files /dev/null and b/Calibre_Plugins/K4PCDeDRM_plugin.zip differ diff --git a/Calibre_Plugins/K4PCDeDRM_plugin/K4PCDeDRM_plugin.py b/Calibre_Plugins/K4PCDeDRM_plugin/K4PCDeDRM_plugin.py new file mode 100644 index 0000000..509987e --- /dev/null +++ b/Calibre_Plugins/K4PCDeDRM_plugin/K4PCDeDRM_plugin.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python +# +# This is a WINDOWS python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# It can run standalone to convert K4PC files, or it can be installed as a +# plugin for Calibre (http://calibre-ebook.com/about) so that importing +# K4PC files with DRM is no londer a multi-step process. +# +# ***NOTE*** Calibre and K4PC must be installed on the same windows machine +# for the plugin version to function properly. +# +# To create a Calibre plugin, rename this file so that the filename +# ends in '_plugin.py', put it into a ZIP file and import that ZIP into Calibre +# using its plugin configuration GUI. +# +# Thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump from +# which this script steals most unashamedly. +# +# Changelog +# 0.01 - Initial version - Utilizes skindle and CMBDTC method of obtaining +# book specific pids from K4PC books. If Calibre and K4PC are installed +# on the same windows machine, Calibre plugin functionality is once +# again restored. + + +""" + +Comprehensive Mazama Book DRM with Topaz Cryptography V2.0 + +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6 +M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC +B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx +y2/pHuYme7U1TsgSjwIDAQAB +-----END PUBLIC KEY----- + +""" + +from __future__ import with_statement + +import csv +import sys +import os +import getopt +import zlib +import binascii +from struct import pack +from struct import unpack +from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ + create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ + string_at, Structure, c_void_p, cast +import _winreg as winreg +import traceback +import hashlib + +__version__ = '0.01' + +global kindleDatabase +MAX_PATH = 255 +kernel32 = windll.kernel32 +advapi32 = windll.advapi32 +crypt32 = windll.crypt32 + + +# +# Various character maps used to decrypt books. Probably supposed to act as obfuscation +# +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" +charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + + +# +# Exceptions for all the problems that might happen during the script +# +class DrmException(Exception): + pass + + +class DataBlob(Structure): + _fields_ = [('cbData', c_uint), + ('pbData', c_void_p)] +DataBlob_p = POINTER(DataBlob) + + +def GetSystemDirectory(): + GetSystemDirectoryW = kernel32.GetSystemDirectoryW + GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] + GetSystemDirectoryW.restype = c_uint + def GetSystemDirectory(): + buffer = create_unicode_buffer(MAX_PATH + 1) + GetSystemDirectoryW(buffer, len(buffer)) + return buffer.value + return GetSystemDirectory +GetSystemDirectory = GetSystemDirectory() + + +def GetVolumeSerialNumber(): + GetVolumeInformationW = kernel32.GetVolumeInformationW + GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, + POINTER(c_uint), POINTER(c_uint), + POINTER(c_uint), c_wchar_p, c_uint] + GetVolumeInformationW.restype = c_uint + def GetVolumeSerialNumber(path): + vsn = c_uint(0) + GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) + return vsn.value + return GetVolumeSerialNumber +GetVolumeSerialNumber = GetVolumeSerialNumber() + + +def GetUserName(): + GetUserNameW = advapi32.GetUserNameW + GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] + GetUserNameW.restype = c_uint + def GetUserName(): + buffer = create_unicode_buffer(32) + size = c_uint(len(buffer)) + while not GetUserNameW(buffer, byref(size)): + buffer = create_unicode_buffer(len(buffer) * 2) + size.value = len(buffer) + return buffer.value.encode('utf-16-le')[::2] + return GetUserName +GetUserName = GetUserName() + + +def CryptUnprotectData(): + _CryptUnprotectData = crypt32.CryptUnprotectData + _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, + c_void_p, c_void_p, c_uint, DataBlob_p] + _CryptUnprotectData.restype = c_uint + def CryptUnprotectData(indata, entropy): + indatab = create_string_buffer(indata) + indata = DataBlob(len(indata), cast(indatab, c_void_p)) + entropyb = create_string_buffer(entropy) + entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) + outdata = DataBlob() + if not _CryptUnprotectData(byref(indata), None, byref(entropy), + None, None, 0, byref(outdata)): + raise DrmException("Failed to Unprotect Data") + return string_at(outdata.pbData, outdata.cbData) + return CryptUnprotectData +CryptUnprotectData = CryptUnprotectData() + + +# +# Returns the MD5 digest of "message" +# +def MD5(message): + ctx = hashlib.md5() + ctx.update(message) + return ctx.digest() + + +# +# Returns the MD5 digest of "message" +# +def SHA1(message): + ctx = hashlib.sha1() + ctx.update(message) + return ctx.digest() + + +# +# Locate and open the Kindle.info file. +# +def openKindleInfo(): + regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") + path = winreg.QueryValueEx(regkey, 'Local AppData')[0] + return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r') + + +# +# Parse the Kindle.info file and return the records as a list of key-values +# +def parseKindleInfo(): + DB = {} + infoReader = openKindleInfo() + infoReader.read(1) + data = infoReader.read() + items = data.split('{') + + for item in items: + splito = item.split(':') + DB[splito[0]] =splito[1] + return DB + + +# +# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal) +# +def findNameForHash(hash): + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + result = "" + for name in names: + if hash == encodeHash(name, charMap2): + result = name + break + return name + + +# +# Print all the records from the kindle.info file. +# +def printKindleInfo(): + for record in kindleDatabase: + name = findNameForHash(record) + if name != "" : + print (name) + print ("--------------------------\n") + else : + print ("Unknown Record") + print getKindleInfoValueForHash(record) + print "\n" + + +# +# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record +# +def getKindleInfoValueForHash(hashedKey): + global kindleDatabase + encryptedValue = decode(kindleDatabase[hashedKey],charMap2) + return CryptUnprotectData(encryptedValue,"") + + +# +# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record +# +def getKindleInfoValueForKey(key): + return getKindleInfoValueForHash(encodeHash(key,charMap2)) + + +# +# 8 bits to six bits encoding from hash to generate PID string +# +def encodePID(hash): + global charMap3 + PID = "" + for position in range (0,8): + PID += charMap3[getSixBitsFromBitField(hash,position)] + return PID + + +# +# Hash the bytes in data and then encode the digest with the characters in map +# +def encodeHash(data,map): + return encode(MD5(data),map) + + +# +# Encode the bytes in data with the characters in map +# +def encode(data, map): + result = "" + for char in data: + value = ord(char) + Q = (value ^ 0x80) // len(map) + R = value % len(map) + result += map[Q] + result += map[R] + return result + + +# +# Decode the string in data with the characters in map. Returns the decoded bytes +# +def decode(data,map): + result = "" + for i in range (0,len(data),2): + high = map.find(data[i]) + low = map.find(data[i+1]) + value = (((high * 0x40) ^ 0x80) & 0xFF) + low + result += pack("B",value) + return result + + +# +# Encryption table used to generate the device PID +# +def generatePidEncryptionTable() : + table = [] + for counter1 in range (0,0x100): + value = counter1 + for counter2 in range (0,8): + if (value & 1 == 0) : + value = value >> 1 + else : + value = value >> 1 + value = value ^ 0xEDB88320 + table.append(value) + return table + + +# +# Seed value used to generate the device PID +# +def generatePidSeed(table,dsn) : + value = 0 + for counter in range (0,4) : + index = (ord(dsn[counter]) ^ value) &0xFF + value = (value >> 8) ^ table[index] + return value + + +# +# Generate the device PID +# +def generateDevicePID(table,dsn,nbRoll): + seed = generatePidSeed(table,dsn) + pidAscii = "" + pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] + index = 0 + + for counter in range (0,nbRoll): + pid[index] = pid[index] ^ ord(dsn[counter]) + index = (index+1) %8 + + for counter in range (0,8): + index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) + pidAscii += charMap4[index] + return pidAscii + + +# +# Returns two bit at offset from a bit field +# +def getTwoBitsFromBitField(bitField,offset): + byteNumber = offset // 4 + bitPosition = 6 - 2*(offset % 4) + + return ord(bitField[byteNumber]) >> bitPosition & 3 + + +# +# Returns the six bits at offset from a bit field +# +def getSixBitsFromBitField(bitField,offset): + offset *= 3 + value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) + return value + + +# +# MobiDeDrm-0.16 Stuff +# +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Multibyte data, if present, is included in the encryption, so + # we do not need to check the low bit. + # if flags & 1: + # num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + + +# +# This class does all the heavy lifting. +# +class DrmStripper: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pid): + pid = pid.ljust(16,'\0') + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + temp_key = PC1(keyvec1, pid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: + found_key = finalkey + break + if not found_key: + # Then try the default encoding that doesn't require a PID + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum: + found_key = finalkey + break + return found_key + + def __init__(self, data_file): + self.data_file = data_file + header = data_file[0:72] + if header[0x3C:0x3C+8] != 'BOOKMOBI': + raise DrmException("invalid file format") + self.num_sections, = unpack('>H', data_file[76:78]) + + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + + sect = self.loadSection(0) + records, = unpack('>H', sect[0x8:0x8+2]) + mobi_length, = unpack('>L',sect[0x14:0x18]) + mobi_version, = unpack('>L',sect[0x68:0x6C]) + extra_data_flags = 0 + print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length) + if (mobi_length >= 0xE4) and (mobi_version >= 5): + extra_data_flags, = unpack('>H', sect[0xF2:0xF4]) + print "Extra Data Flags = %d" %extra_data_flags + + crypto_type, = unpack('>H', sect[0xC:0xC+2]) + if crypto_type == 0: + print "This book is not encrypted." + else: + if crypto_type == 1: + raise DrmException("cannot decode Mobipocket encryption type 1") + if crypto_type != 2: + raise DrmException("unknown encryption type: %d" % crypto_type) + + # determine the EXTH Offset. + exth_off = unpack('>I', sect[20:24])[0] + 16 + self.sections[0][0] + # Grab the entire EXTH block and feed it to the getK4PCPids function. + exth = data_file[exth_off:self.sections[0+1][0]] + pid = getK4PCPids(exth) + + # calculate the keys + drm_ptr, drm_count, drm_size, drm_flags = unpack('>LLLL', sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("no PIDs found in this file") + found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) + if not found_key: + raise DrmException("no key found. maybe the PID is incorrect") + + # kill the drm keys + self.patchSection(0, "\0" * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print "\nDecrypting. Please wait . . .", + new_data = self.data_file[:self.sections[1][0]] + for i in xrange(1, records+1): + data = self.loadSection(i) + extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags) + if i%100 == 0: + print ".", + # print "record %d, extra_size %d" %(i,extra_size) + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done!" + print "\nPlease only use your new-found powers for good." + + def getResult(self): + return self.data_file + + +# +# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo +# file to calculate the book pid. +# +def getK4PCPids(exth): + global kindleDatabase + try: + kindleDatabase = parseKindleInfo() + except Exception as message: + print(message) + + if kindleDatabase != None : + + # Get the Mazama Random number + MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber") + + # Get the HDD serial + encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1) + + # Get the current user name + encodedUsername = encodeHash(GetUserName(),charMap1) + + # concat, hash and encode to calculate the DSN + DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + + print("\nDSN: " + DSN) + + + # Compute the device PID (for which I can tell, is used for nothing). + # But hey, stuff being printed out is apparently cool. + table = generatePidEncryptionTable() + devicePID = generateDevicePID(table,DSN,4) + + print("Device PID: " + devicePID) + + # Compute book PID + exth_records = {} + nitems, = unpack('>I', exth[8:12]) + pos = 12 + # Parse the EXTH records, storing data indexed by type + for i in xrange(nitems): + type, size = unpack('>II', exth[pos: pos + 8]) + content = exth[pos + 8: pos + size] + + exth_records[type] = content + pos += size + + # Grab the contents of the type 209 exth record + if exth_records[209] != None: + data = exth_records[209] + else: + raise DrmException("\nNo EXTH record type 209 - Perhaps not a K4PC file?") + + # Parse the 209 data to find the the exth record with the token data. + # The last character of the 209 data points to the record with the token. + # Always 208 from my experience, but I'll leave the logic in case that changes. + for i in xrange(len(data)): + if ord(data[i]) != 0: + if exth_records[ord(data[i])] != None: + token = exth_records[ord(data[i])] + + # Get the kindle account token + kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens") + + print("Account Token: " + kindleAccountToken) + + pidHash = SHA1(DSN+kindleAccountToken+exth_records[209]+token) + + bookPID = encodePID(pidHash) + + if exth_records[503] != None: + print "Pid for " + exth_records[503] + ": " + bookPID + else: + print ("Book PID: " + bookPID ) + + return bookPID + + raise DrmException("\nCould not access K4PC data - Perhaps K4PC is not installed/configured?") + return null + +if not __name__ == "__main__": + from calibre.customize import FileTypePlugin + + class K4PCDeDRM(FileTypePlugin): + name = 'K4PCDeDRM' # Name of the plugin + description = 'Removes DRM from K4PC files' + supported_platforms = ['windows'] # Platforms this plugin will run on + author = 'DiapDealer' # The author of this plugin + version = (0, 0, 1) # The version number of this plugin + file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to + on_import = True # Run this plugin during the import + + def run(self, path_to_ebook): + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + data_file = file(path_to_ebook, 'rb').read() + + try: + unlocked_file = DrmStripper(data_file).getResult() + except DrmException: + # ignore the error + pass + else: + of = self.temporary_file('.mobi') + of.write(unlocked_file) + of.close() + return of.name + + if is_ok_to_use_qt(): + d = QMessageBox(QMessageBox.Warning, "K4PCDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook) + d.show() + d.raise_() + d.exec_() + return path_to_ebook + + #def customization_help(self, gui=False): + # return 'Enter PID (separate multiple PIDs with comma)' + +if __name__ == "__main__": + sys.stdout=Unbuffered(sys.stdout) + print ('K4PCDeDrm v%(__version__)s ' + 'provided DiapDealer.' % globals()) + if len(sys.argv)<3: + print "Removes DRM protection from K4PC books" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + infile = sys.argv[1] + outfile = sys.argv[2] + data_file = file(infile, 'rb').read() + try: + strippedFile = DrmStripper(data_file) + file(outfile, 'wb').write(strippedFile.getResult()) + except DrmException, e: + print "Error: %s" % e + sys.exit(1) + sys.exit(0) \ No newline at end of file diff --git a/Calibre_Plugins/MobiDeDRM_plugin.zip b/Calibre_Plugins/MobiDeDRM_plugin.zip new file mode 100644 index 0000000..dc184a3 Binary files /dev/null and b/Calibre_Plugins/MobiDeDRM_plugin.zip differ diff --git a/Kindle_Mobi_Tools/unswindle/mobidedrm.py b/Calibre_Plugins/MobiDeDRM_plugin/MobiDeDRM_plugin.py similarity index 100% rename from Kindle_Mobi_Tools/unswindle/mobidedrm.py rename to Calibre_Plugins/MobiDeDRM_plugin/MobiDeDRM_plugin.py diff --git a/Calibre_Plugins/README-K4PCDeDRM-plugin.txt b/Calibre_Plugins/README-K4PCDeDRM-plugin.txt new file mode 100644 index 0000000..229dbd4 --- /dev/null +++ b/Calibre_Plugins/README-K4PCDeDRM-plugin.txt @@ -0,0 +1,12 @@ +K4PCDeDRM - K4PCDeDRM_X.XX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +This work is based on the work of cmbtc, skindle, mobidedrm. and skindleAll I had the much easier job of converting them to a Calibre plugin. + +This plugin is meant to Kindle for PC azw ebooks that are protected +with Amazon's Mobi based encryption. It is meant to function without having to install any dependencies... other than having both Calibre installed and Kindle for PC on the same machine, of course. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (K4PCDeDRM_X.XX_plugin.zip) and click the 'Add' button. you're done. + diff --git a/Calibre_Plugins/README-ignobleepub-plugin.txt b/Calibre_Plugins/README-ignobleepub-plugin.txt new file mode 100644 index 0000000..6fd92cd --- /dev/null +++ b/Calibre_Plugins/README-ignobleepub-plugin.txt @@ -0,0 +1,38 @@ +Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +All credit given to I <3 Cabbages for the original standalone scripts. +I had the much easier job of converting them to a Calibre plugin. + +This plugin is meant to decrypt Barnes & Noble Epubs that are protected +with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and +click the 'Add' button. you're done. + +Configuration: + +1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on +Calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234 + +If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345 + +** NOTE ** The above method is your only option if you don't have/can't run the original I <3 Cabbages scripts on your particular machine. + +** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below. + +2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. + +All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. diff --git a/Calibre_Plugins/README-ineptepub-plugin.txt b/Calibre_Plugins/README-ineptepub-plugin.txt new file mode 100644 index 0000000..3777939 --- /dev/null +++ b/Calibre_Plugins/README-ineptepub-plugin.txt @@ -0,0 +1,36 @@ +Inept Epub DeDRM - ineptepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +All credit given to I <3 Cabbages for the original standalone scripts. +I had the much easier job of converting them to a Calibre plugin. + +This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done. + +Configuration: + +When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to +find the Adobe Digital Editions installation installation. + +So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. + +If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that +they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. + +Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. + +All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book. + +** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. \ No newline at end of file diff --git a/Calibre_Plugins/README_MobiDeDRM_plugin.txt b/Calibre_Plugins/README_MobiDeDRM_plugin.txt new file mode 100644 index 0000000..eaef29e --- /dev/null +++ b/Calibre_Plugins/README_MobiDeDRM_plugin.txt @@ -0,0 +1,13 @@ +MobiDeDRM - MobiDeDRM_X.XX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +This work is based on the current mobidedrm.py code. + +This plugin is meant to Mobipocket and Kindle ebooks that are protected +with Amazon's Mobi based encryption. It is meant to function without having to install any dependencies... other than having both Calibre installed. You must know the PID orf the device you are using or the book specific PID to use this plugin. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (MobiDeDRM_X.XX_plugin.zip) and click the 'Add' button. + +Then enter your PIDS in the plugin customization window separated by commas (with no spaces). \ No newline at end of file diff --git a/Calibre_Plugins/ignobleepub_plugin.zip b/Calibre_Plugins/ignobleepub_plugin.zip new file mode 100644 index 0000000..a043ceb Binary files /dev/null and b/Calibre_Plugins/ignobleepub_plugin.zip differ diff --git a/Calibre_Plugins/ignobleepub_plugin/Ignobleepub-README.txt b/Calibre_Plugins/ignobleepub_plugin/Ignobleepub-README.txt new file mode 100644 index 0000000..8ec9d1e --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/Ignobleepub-README.txt @@ -0,0 +1,65 @@ +Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +All credit given to I <3 Cabbages for the original standalone scripts. +I had the much easier job of converting them to a Calibre plugin. + +This plugin is meant to decrypt Barnes & Noble Epubs that are protected +with Adobe's Adept encryption. It is meant to function without having to install +any dependencies... other than having Calibre installed, of course. It will still +work if you have Python and PyCrypto already installed, but they aren't necessary. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file +dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and +click the 'Add' button. you're done. + +Configuration: + +1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account +name) and credit card number (the one used to purchase the books) into the plugin's +customization window. It's the same info you would enter into the ignoblekeygen script. +Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on +Calibre's Preferences->Plugins page. Enter the name and credit card number separated +by a comma: Your Name,1234123412341234 + +If you've purchased books with more than one credit card, separate that other info with +a colon: Your Name,1234123412341234:Other Name,2345234523452345 + +** NOTE ** The above method is your only option if you don't have/can't run the original +I <3 Cabbages scripts on your particular machine. + +** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration +page when using the above method. If other people have access to your computer, +you may want to use the second configuration method below. + +2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw +script, you can put those keyfiles into Calibre's configuration directory. The easiest +way to find the correct directory is to go to Calibre's Preferences page... click +on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +configuration directory' button. Paste your keyfiles in there. Just make sure that +they have different names and are saved with the '.b64' extension (like the ignoblekeygen +script produces). This directory isn't touched when upgrading Calibre, so it's quite safe +to leave then there. + +All keyfiles from method 2 and all data entered from method 1 will be used to attempt +to decrypt a book. You can use method 1 or method 2, or a combination of both. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can +save a lot of time and trouble by trying to add the epub to Calibre with the command +line tools. This will print out a lot of helpful debugging info that can be copied into +any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're +trying to import resides. Then type the command "calibredb add your_ebook.epub". +Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the +filename of your book is. Copy the resulting output and paste it into any online +help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. +If you go to the 'Preferences' page and click on the miscellaneous button, you'll +see the option to install the command line tools. diff --git a/Calibre_Plugins/ignobleepub_plugin/ignobleepub_plugin.py b/Calibre_Plugins/ignobleepub_plugin/ignobleepub_plugin.py new file mode 100644 index 0000000..5147f06 --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/ignobleepub_plugin.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python + +# ignobleepub_v01_plugin.py +# Released under the terms of the GNU General Public Licence, version 3 or +# later. +# +# Requires Calibre version 0.6.44 or higher. +# +# All credit given to I <3 Cabbages for the original standalone scripts. +# I had the much easier job of converting them to Calibre a plugin. +# +# This plugin is meant to decrypt Barnes & Noble Epubs that are protected +# with Adobe's Adept encryption. It is meant to function without having to install +# any dependencies... other than having Calibre installed, of course. It will still +# work if you have Python and PyCrypto already installed, but they aren't necessary. +# +# Configuration: +# 1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account +# name) and credit card number (the one used to purchase the books) into the plugin's +# customization window. Highlight the plugin (Ignoble Epub DeDRM) and click the +# "Customize Plugin" button on Calibre's Preferences->Plugins page. +# Enter the name and credit card number separated by a comma: Your Name,1234123412341234 +# +# If you've purchased books with more than one credit card, separate the info with +# a colon: Your Name,1234123412341234:Other Name,2345234523452345 +# +# ** Method 1 is your only option if you don't have/can't run the original +# I <3 Cabbages scripts on your particular machine. ** +# +# 2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw +# script, you can put those keyfiles in Calibre's configuration directory. The easiest +# way to find the correct directory is to go to Calibre's Preferences page... click +# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +# configuration directory' button. Paste your keyfiles in there. Just make sure that +# they have different names and are saved with the '.b64' extension (like the ignoblekeygen +# script produces). This directory isn't touched when upgrading Calibre, so it's quite safe +# to leave then there. +# +# All keyfiles from option 2 and all data entered from option 1 will be used to attempt +# to decrypt a book. You can use option 1 or option 2, or a combination of both. +# +# +# Revision history: +# 0.1 - Initial release + + +""" +Decrypt Barnes & Noble ADEPT encrypted EPUB books. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import hashlib +import zlib +import zipfile +import re +from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED +import xml.etree.ElementTree as etree +from contextlib import closing + +global AES +global AES2 + +META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +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 + + 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_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 IGNOBLEError('AES improper key used') + return + key = self._key = AES_KEY() + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) + if rv < 0: + raise IGNOBLEError('Failed to initialize AES key') + + def decrypt(self, data): + out = create_string_buffer(len(data)) + iv = ("\x00" * self._blocksize) + rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) + if rv == 0: + raise IGNOBLEError('AES decryption failed') + return out.raw + + class AES2(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 + print 'IgnobleEpub: Using libcrypto.' + return (AES, AES2) + +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_CBC) + + def decrypt(self, data): + return self._aes.decrypt(data) + + class AES2(object): + def __init__(self, key, iv): + self._aes = _AES.new(key, _AES.MODE_CBC, iv) + + def encrypt(self, data): + return self._aes.encrypt(data) + print 'IgnobleEpub: Using PyCrypto.' + return (AES, AES2) + +def _load_crypto(): + _aes = _aes2 = None + for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto): + try: + _aes, _aes2 = loader() + break + except (ImportError, IGNOBLEError): + pass + return (_aes, _aes2) + +def normalize_name(name): # Strip spaces and convert to lowercase. + return ''.join(x for x in name.lower() if x != ' ') + +def generate_keyfile(name, ccn): + name = normalize_name(name) + '\x00' + ccn = ccn + '\x00' + name_sha = hashlib.sha1(name).digest()[:16] + ccn_sha = hashlib.sha1(ccn).digest()[:16] + both_sha = hashlib.sha1(name + ccn).digest() + aes = AES2(ccn_sha, name_sha) + crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) + userkey = hashlib.sha1(crypt).digest() + + return userkey.encode('base64') + +class ZipInfo(zipfile.ZipInfo): + def __init__(self, *args, **kwargs): + if 'compress_type' in kwargs: + compress_type = kwargs.pop('compress_type') + super(ZipInfo, self).__init__(*args, **kwargs) + self.compress_type = compress_type + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES(bookkey) + encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in encryption.findall(expr): + path = elem.get('URI', None) + if path is not None: + encrypted.add(path) + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + bytes = dc.decompress(bytes) + ex = dc.decompress('Z') + dc.flush() + if ex: + bytes = bytes + ex + return bytes + + def decrypt(self, path, data): + if path in self._encrypted: + data = self._aes.decrypt(data)[16:] + data = data[:-ord(data[-1])] + data = self.decompress(data) + return data + +def plugin_main(userkey, inpath, outpath): + key = userkey.decode('base64')[:16] + aes = AES(key) + + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + return 1 + for name in META_NAMES: + namelist.remove(name) + try: # If the generated keyfile doesn't match the bookkey, this is where it's likely to blow up. + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + bookkey = aes.decrypt(bookkey.decode('base64')) + bookkey = bookkey[:-ord(bookkey[-1])] + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey[-16:], encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + zi = ZipInfo('mimetype', compress_type=ZIP_STORED) + outf.writestr(zi, inf.read('mimetype')) + for path in namelist: + data = inf.read(path) + outf.writestr(path, decryptor.decrypt(path, data)) + except: + return 2 + return 0 + +from calibre.customize import FileTypePlugin + +class IgnobleDeDRM(FileTypePlugin): + name = 'Ignoble Epub DeDRM' + description = 'Removes DRM from secure Barnes & Noble epub files. \ + Credit given to I <3 Cabbages for the original stand-alone scripts.' + supported_platforms = ['linux', 'osx', 'windows'] + author = 'DiapDealer' + version = (0, 1, 0) + minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions. + file_types = set(['epub']) + on_import = True + + def run(self, path_to_ebook): + global AES + global AES2 + + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + from calibre.constants import iswindows, isosx + + # Add the included pycrypto import directory for Windows users. + pdir = 'windows' if iswindows else 'osx' if isosx else 'linux' + ppath = os.path.join(self.sys_insertion_path, pdir) + #sys.path.insert(0, ppath) + sys.path.append(ppath) + + AES, AES2 = _load_crypto() + + if AES == None or AES2 == None: + # Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.' + sys.path.remove(ppath) + raise IGNOBLEError('IgnobleEpub - Failed to load crypto libs.') + return + + # Load any keyfiles (*.b64) included Calibre's config directory. + userkeys = [] + try: + # Find Calibre's configuration directory. + confpath = os.path.split(os.path.split(self.plugin_path)[0])[0] + print 'IgnobleEpub: Calibre configuration directory = %s' % confpath + files = os.listdir(confpath) + filefilter = re.compile("\.b64$", re.IGNORECASE) + files = filter(filefilter.search, files) + + if files: + for filename in files: + fpath = os.path.join(confpath, filename) + with open(fpath, 'rb') as f: + userkeys.append(f.read()) + print 'IgnobleEpub: Keyfile %s found in config folder.' % filename + else: + print 'IgnobleEpub: No keyfiles found. Checking plugin customization string.' + except IOError: + print 'IgnobleEpub: Error reading keyfiles from config directory.' + pass + + # Get name and credit card number from Plugin Customization + if not userkeys and not self.site_customization: + # Plugin hasn't been configured... do nothing. + sys.path.remove(ppath) + raise IGNOBLEError('IgnobleEpub - No keys found. Plugin not configured.') + return + + if self.site_customization: + keystuff = self.site_customization + ar = keystuff.split(':') + keycount = 0 + for i in ar: + try: + name, ccn = i.split(',') + keycount += 1 + except ValueError: + sys.path.remove(ppath) + raise IGNOBLEError('IgnobleEpub - Error parsing user supplied data.') + return + + # Generate Barnes & Noble EPUB user key from name and credit card number. + userkeys.append( generate_keyfile(name, ccn) ) + print 'IgnobleEpub: %d userkey(s) generated from customization data.' % keycount + + # Attempt to decrypt epub with each encryption key (generated or provided). + for userkey in userkeys: + # Create a TemporaryPersistent file to work with. + of = self.temporary_file('.epub') + + # Give the user key, ebook and TemporaryPersistent file to the Stripper function. + result = plugin_main(userkey, path_to_ebook, of.name) + + # Ebook is not a B&N Adept epub... do nothing and pass it on. + # This allows a non-encrypted epub to be imported without error messages. + if result == 1: + print 'IgnobleEpub: Not a B&N Adept Epub... punting.' + of.close() + sys.path.remove(ppath) + return path_to_ebook + break + + # Decryption was successful return the modified PersistentTemporary + # file to Calibre's import process. + if result == 0: + print 'IgnobleEpub: Encryption successfully removed.' + of.close() + sys.path.remove(ppath) + return of.name + break + + print 'IgnobleEpub: Encryption key invalid... trying others.' + of.close() + + # Something went wrong with decryption. + # Import the original unmolested epub. + of.close + sys.path.remove(ppath) + raise IGNOBLEError('IgnobleEpub - Ultimately failed to decrypt.') + return + + + def customization_help(self, gui=False): + return 'Enter B&N Account name and CC# (separate name and CC# with a comma)' \ No newline at end of file diff --git a/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/Cipher/__init__.py b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/Cipher/__init__.py new file mode 100644 index 0000000..d8ceed9 --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/Cipher/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Secret-key encryption algorithms. + +Secret-key encryption algorithms transform plaintext in some way that +is dependent on a key, producing ciphertext. This transformation can +easily be reversed, if (and, hopefully, only if) one knows the key. + +The encryption modules here all support the interface described in PEP +272, "API for Block Encryption Algorithms". + +If you don't know which algorithm to choose, use AES because it's +standard and has undergone a fair bit of examination. + +Crypto.Cipher.AES Advanced Encryption Standard +Crypto.Cipher.ARC2 Alleged RC2 +Crypto.Cipher.ARC4 Alleged RC4 +Crypto.Cipher.Blowfish +Crypto.Cipher.CAST +Crypto.Cipher.DES The Data Encryption Standard. Very commonly used + in the past, but today its 56-bit keys are too small. +Crypto.Cipher.DES3 Triple DES. +Crypto.Cipher.XOR The simple XOR cipher. +""" + +__all__ = ['AES', 'ARC2', 'ARC4', + 'Blowfish', 'CAST', 'DES', 'DES3', + 'XOR' + ] + +__revision__ = "$Id$" + + diff --git a/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/__init__.py b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/__init__.py new file mode 100644 index 0000000..d596e4f --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Python Cryptography Toolkit + +A collection of cryptographic modules implementing various algorithms +and protocols. + +Subpackages: +Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4) +Crypto.Hash Hashing algorithms (MD5, SHA, HMAC) +Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing + transform). This package does not contain any + network protocols. +Crypto.PublicKey Public-key encryption and signature algorithms + (RSA, DSA) +Crypto.Util Various useful modules and functions (long-to-string + conversion, random number generation, number + theoretic functions) +""" + +__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util'] + +__version__ = '2.3' # See also below and setup.py +__revision__ = "$Id$" + +# New software should look at this instead of at __version__ above. +version_info = (2, 1, 0, 'final', 0) # See also above and setup.py + diff --git a/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/pct_warnings.py b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/pct_warnings.py new file mode 100644 index 0000000..2a5716c --- /dev/null +++ b/Calibre_Plugins/ignobleepub_plugin/windows/Crypto/pct_warnings.py @@ -0,0 +1,57 @@ +# -*- coding: ascii -*- +# +# pct_warnings.py : PyCrypto warnings file +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +# +# Base classes. All our warnings inherit from one of these in order to allow +# the user to specifically filter them. +# + +class CryptoWarning(Warning): + """Base class for PyCrypto warnings""" + +class CryptoDeprecationWarning(DeprecationWarning, CryptoWarning): + """Base PyCrypto DeprecationWarning class""" + +class CryptoRuntimeWarning(RuntimeWarning, CryptoWarning): + """Base PyCrypto RuntimeWarning class""" + +# +# Warnings that we might actually use +# + +class RandomPool_DeprecationWarning(CryptoDeprecationWarning): + """Issued when Crypto.Util.randpool.RandomPool is instantiated.""" + +class ClockRewindWarning(CryptoRuntimeWarning): + """Warning for when the system clock moves backwards.""" + +class GetRandomNumber_DeprecationWarning(CryptoDeprecationWarning): + """Issued when Crypto.Util.number.getRandomNumber is invoked.""" + +# By default, we want this warning to be shown every time we compensate for +# clock rewinding. +import warnings as _warnings +_warnings.filterwarnings('always', category=ClockRewindWarning, append=1) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin.zip b/Calibre_Plugins/ineptepub_plugin.zip new file mode 100644 index 0000000..4b18854 Binary files /dev/null and b/Calibre_Plugins/ineptepub_plugin.zip differ diff --git a/Calibre_Plugins/ineptepub_plugin/Ineptepub-README.txt b/Calibre_Plugins/ineptepub_plugin/Ineptepub-README.txt new file mode 100644 index 0000000..df7e31b --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/Ineptepub-README.txt @@ -0,0 +1,62 @@ +Inept Epub DeDRM - ineptepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +All credit given to I <3 Cabbages for the original standalone scripts. +I had the much easier job of converting them to a Calibre plugin. + +This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected +with Adobe's Adept encryption. It is meant to function without having to install +any dependencies... other than having Calibre installed, of course. It will still +work if you have Python and PyCrypto already installed, but they aren't necessary. + +Installation: + +Go to Calibre's Preferences page... click on the Plugins button. Use the file +dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and +click the 'Add' button. you're done. + +Configuration: + +When first run, the plugin will attempt to find your Adobe Digital Editions installation +(on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and +save it in Calibre's configuration directory. It will use that file on subsequent runs. +If there are already '*.der' files in the directory, the plugin won't attempt to +find the Adobe Digital Editions installation installation. + +So if you have Adobe Digital Editions installation installed on the same machine as Calibre... +you are ready to go. If not... keep reading. + +If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, +you can put those keyfiles in Calibre's configuration directory. The easiest +way to find the correct directory is to go to Calibre's Preferences page... click +on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +configuration directory' button. Paste your keyfiles in there. Just make sure that +they have different names and are saved with the '.der' extension (like the ineptkey +script produces). This directory isn't touched when upgrading Calibre, so it's quite +safe to leave them there. + +Since there is no Linux version of Adobe Digital Editions, Linux users will have to +obtain a keyfile through other methods and put the file in Calibre's configuration directory. + +All keyfiles with a '.der' extension found in Calibre's configuration directory will +be used to attempt to decrypt a book. + +** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can +save a lot of time and trouble by trying to add the epub to Calibre with the command +line tools. This will print out a lot of helpful debugging info that can be copied into +any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're +trying to import resides. Then type the command "calibredb add your_ebook.epub". +Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the +filename of your book is. Copy the resulting output and paste it into any online +help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. +If you go to the 'Preferences' page and click on the miscellaneous button, you'll +see the option to install the command line tools. \ No newline at end of file diff --git a/Calibre_Plugins/ineptepub_plugin/ade_key.py b/Calibre_Plugins/ineptepub_plugin/ade_key.py new file mode 100644 index 0000000..fa63c37 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/ade_key.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python + +""" +Retrieve Adobe ADEPT user key. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import struct +from calibre.constants import iswindows, isosx + +class ADEPTError(Exception): + pass + +if iswindows: + from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ + create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ + string_at, Structure, c_void_p, cast, c_size_t, memmove + from ctypes.wintypes import LPVOID, DWORD, BOOL + import _winreg as winreg + + try: + from Crypto.Cipher import AES as _aes + except ImportError: + _aes = None + + DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device' + PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' + + MAX_PATH = 255 + + kernel32 = windll.kernel32 + advapi32 = windll.advapi32 + crypt32 = windll.crypt32 + + def GetSystemDirectory(): + GetSystemDirectoryW = kernel32.GetSystemDirectoryW + GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] + GetSystemDirectoryW.restype = c_uint + def GetSystemDirectory(): + buffer = create_unicode_buffer(MAX_PATH + 1) + GetSystemDirectoryW(buffer, len(buffer)) + return buffer.value + return GetSystemDirectory + GetSystemDirectory = GetSystemDirectory() + + def GetVolumeSerialNumber(): + GetVolumeInformationW = kernel32.GetVolumeInformationW + GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, + POINTER(c_uint), POINTER(c_uint), + POINTER(c_uint), c_wchar_p, c_uint] + GetVolumeInformationW.restype = c_uint + def GetVolumeSerialNumber(path): + vsn = c_uint(0) + GetVolumeInformationW( + path, None, 0, byref(vsn), None, None, None, 0) + return vsn.value + return GetVolumeSerialNumber + GetVolumeSerialNumber = GetVolumeSerialNumber() + + def GetUserName(): + GetUserNameW = advapi32.GetUserNameW + GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] + GetUserNameW.restype = c_uint + def GetUserName(): + buffer = create_unicode_buffer(32) + size = c_uint(len(buffer)) + while not GetUserNameW(buffer, byref(size)): + buffer = create_unicode_buffer(len(buffer) * 2) + size.value = len(buffer) + return buffer.value.encode('utf-16-le')[::2] + return GetUserName + GetUserName = GetUserName() + + PAGE_EXECUTE_READWRITE = 0x40 + MEM_COMMIT = 0x1000 + MEM_RESERVE = 0x2000 + + def VirtualAlloc(): + _VirtualAlloc = kernel32.VirtualAlloc + _VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD] + _VirtualAlloc.restype = LPVOID + def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE), + protect=PAGE_EXECUTE_READWRITE): + return _VirtualAlloc(addr, size, alloctype, protect) + return VirtualAlloc + VirtualAlloc = VirtualAlloc() + + MEM_RELEASE = 0x8000 + + def VirtualFree(): + _VirtualFree = kernel32.VirtualFree + _VirtualFree.argtypes = [LPVOID, c_size_t, DWORD] + _VirtualFree.restype = BOOL + def VirtualFree(addr, size=0, freetype=MEM_RELEASE): + return _VirtualFree(addr, size, freetype) + return VirtualFree + VirtualFree = VirtualFree() + + class NativeFunction(object): + def __init__(self, restype, argtypes, insns): + self._buf = buf = VirtualAlloc(None, len(insns)) + memmove(buf, insns, len(insns)) + ftype = CFUNCTYPE(restype, *argtypes) + self._native = ftype(buf) + + def __call__(self, *args): + return self._native(*args) + + def __del__(self): + if self._buf is not None: + VirtualFree(self._buf) + self._buf = None + + if struct.calcsize("P") == 4: + CPUID0_INSNS = ( + "\x53" # push %ebx + "\x31\xc0" # xor %eax,%eax + "\x0f\xa2" # cpuid + "\x8b\x44\x24\x08" # mov 0x8(%esp),%eax + "\x89\x18" # mov %ebx,0x0(%eax) + "\x89\x50\x04" # mov %edx,0x4(%eax) + "\x89\x48\x08" # mov %ecx,0x8(%eax) + "\x5b" # pop %ebx + "\xc3" # ret + ) + CPUID1_INSNS = ( + "\x53" # push %ebx + "\x31\xc0" # xor %eax,%eax + "\x40" # inc %eax + "\x0f\xa2" # cpuid + "\x5b" # pop %ebx + "\xc3" # ret + ) + else: + CPUID0_INSNS = ( + "\x49\x89\xd8" # mov %rbx,%r8 + "\x49\x89\xc9" # mov %rcx,%r9 + "\x48\x31\xc0" # xor %rax,%rax + "\x0f\xa2" # cpuid + "\x4c\x89\xc8" # mov %r9,%rax + "\x89\x18" # mov %ebx,0x0(%rax) + "\x89\x50\x04" # mov %edx,0x4(%rax) + "\x89\x48\x08" # mov %ecx,0x8(%rax) + "\x4c\x89\xc3" # mov %r8,%rbx + "\xc3" # retq + ) + CPUID1_INSNS = ( + "\x53" # push %rbx + "\x48\x31\xc0" # xor %rax,%rax + "\x48\xff\xc0" # inc %rax + "\x0f\xa2" # cpuid + "\x5b" # pop %rbx + "\xc3" # retq + ) + + def cpuid0(): + _cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS) + buf = create_string_buffer(12) + def cpuid0(): + _cpuid0(buf) + return buf.raw + return cpuid0 + cpuid0 = cpuid0() + + cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS) + + class DataBlob(Structure): + _fields_ = [('cbData', c_uint), + ('pbData', c_void_p)] + DataBlob_p = POINTER(DataBlob) + + def CryptUnprotectData(): + _CryptUnprotectData = crypt32.CryptUnprotectData + _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, + c_void_p, c_void_p, c_uint, DataBlob_p] + _CryptUnprotectData.restype = c_uint + def CryptUnprotectData(indata, entropy): + indatab = create_string_buffer(indata) + indata = DataBlob(len(indata), cast(indatab, c_void_p)) + entropyb = create_string_buffer(entropy) + entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) + outdata = DataBlob() + if not _CryptUnprotectData(byref(indata), None, byref(entropy), + None, None, 0, byref(outdata)): + raise ADEPTError("Failed to decrypt user key key (sic)") + return string_at(outdata.pbData, outdata.cbData) + return CryptUnprotectData + CryptUnprotectData = CryptUnprotectData() + + def retrieve_key(): + if _aes is None: + raise ADEPTError("Couldn\'t load PyCrypto") + root = GetSystemDirectory().split('\\')[0] + '\\' + serial = GetVolumeSerialNumber(root) + vendor = cpuid0() + signature = struct.pack('>I', cpuid1())[1:] + user = GetUserName() + entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) + cuser = winreg.HKEY_CURRENT_USER + try: + regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) + except WindowsError: + raise ADEPTError("Adobe Digital Editions not activated") + device = winreg.QueryValueEx(regkey, 'key')[0] + keykey = CryptUnprotectData(device, entropy) + userkey = None + try: + plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) + except WindowsError: + raise ADEPTError("Could not locate ADE activation") + for i in xrange(0, 16): + try: + plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) + except WindowsError: + break + ktype = winreg.QueryValueEx(plkparent, None)[0] + if ktype != 'credentials': + continue + for j in xrange(0, 16): + try: + plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) + except WindowsError: + break + ktype = winreg.QueryValueEx(plkkey, None)[0] + if ktype != 'privateLicenseKey': + continue + userkey = winreg.QueryValueEx(plkkey, 'value')[0] + break + if userkey is not None: + break + if userkey is None: + raise ADEPTError('Could not locate privateLicenseKey') + userkey = userkey.decode('base64') + userkey = _aes.new(keykey, _aes.MODE_CBC).decrypt(userkey) + userkey = userkey[26:-ord(userkey[-1])] + return userkey + +else: + + import xml.etree.ElementTree as etree + import Carbon.File + import Carbon.Folder + import Carbon.Folders + import MacOS + + ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat' + NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + + def find_folder(domain, dtype): + try: + fsref = Carbon.Folder.FSFindFolder(domain, dtype, False) + return Carbon.File.pathname(fsref) + except MacOS.Error: + return None + + def find_app_support_file(subpath): + dtype = Carbon.Folders.kApplicationSupportFolderType + for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain: + path = find_folder(domain, dtype) + if path is None: + continue + path = os.path.join(path, subpath) + if os.path.isfile(path): + return path + return None + + def retrieve_key(): + actpath = find_app_support_file(ACTIVATION_PATH) + if actpath is None: + raise ADEPTError("Could not locate ADE activation") + tree = etree.parse(actpath) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) + userkey = tree.findtext(expr) + userkey = userkey.decode('base64') + userkey = userkey[26:] + return userkey diff --git a/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py b/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py new file mode 100644 index 0000000..c9cc4bd --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py @@ -0,0 +1,468 @@ +#! /usr/bin/python + +# ineptepub_v01_plugin.py +# Released under the terms of the GNU General Public Licence, version 3 or +# later. +# +# Requires Calibre version 0.6.44 or higher. +# +# All credit given to I <3 Cabbages for the original standalone scripts. +# I had the much easier job of converting them to a Calibre plugin. +# +# This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected +# with Adobe's Adept encryption. It is meant to function without having to install +# any dependencies... other than having Calibre installed, of course. It will still +# work if you have Python and PyCrypto already installed, but they aren't necessary. +# +# Configuration: +# When first run, the plugin will attempt to find your Adobe Digital Editions installation +# (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and +# save it in Calibre's configuration directory. It will use that file on subsequent runs. +# If there are already '*.der' files in the directory, the plugin won't attempt to +# find the ADE installation. So if you have ADE installed on the same machine as Calibre... +# you are ready to go. +# +# If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, +# you can put those keyfiles in Calibre's configuration directory. The easiest +# way to find the correct directory is to go to Calibre's Preferences page... click +# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +# configuration directory' button. Paste your keyfiles in there. Just make sure that +# they have different names and are saved with the '.der' extension (like the ineptkey +# script produces). This directory isn't touched when upgrading Calibre, so it's quite +# safe to leave them there. +# +# Since there is no Linux version of Adobe Digital Editions, Linux users will have to +# obtain a keyfile through other methods and put the file in Calibre's configuration directory. +# +# All keyfiles with a '.der' extension found in Calibre's configuration directory will +# be used to attempt to decrypt a book. +# +# ** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. +# +# Revision history: +# 0.1 - Initial release + + +""" +Decrypt Adobe ADEPT-encrypted EPUB books. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import zlib +import zipfile +import re +from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED +from contextlib import closing +import xml.etree.ElementTree as etree + +global AES +global RSA + +META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + + +class ADEPTError(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 + + 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 = ("\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 + print 'IneptEpub: Using libcrypto.' + return (AES, RSA) + +def _load_crypto_pycrypto(): + from Crypto.Cipher import AES as _AES + from Crypto.PublicKey import RSA as _RSA + + # 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) + + def decrypt(self, data): + return self._aes.decrypt(data) + + class RSA(object): + def __init__(self, der): + key = ASN1Parser([ord(x) for x in der]) + key = [key.getChild(x).value for x in xrange(1, 4)] + key = [self.bytesToNumber(v) for v in key] + self._rsa = _RSA.construct(key) + + def bytesToNumber(self, bytes): + total = 0L + for byte in bytes: + total = (total << 8) + byte + return total + + def decrypt(self, data): + return self._rsa.decrypt(data) + print 'IneptEpub: Using pycrypto.' + return (AES, RSA) + +def _load_crypto(): + _aes = _rsa = None + for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto): + try: + _aes, _rsa = loader() + break + except (ImportError, ADEPTError): + pass + return (_aes, _rsa) + +class ZipInfo(zipfile.ZipInfo): + def __init__(self, *args, **kwargs): + if 'compress_type' in kwargs: + compress_type = kwargs.pop('compress_type') + super(ZipInfo, self).__init__(*args, **kwargs) + self.compress_type = compress_type + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES(bookkey) + encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in encryption.findall(expr): + path = elem.get('URI', None) + if path is not None: + encrypted.add(path) + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + bytes = dc.decompress(bytes) + ex = dc.decompress('Z') + dc.flush() + if ex: + bytes = bytes + ex + return bytes + + def decrypt(self, path, data): + if path in self._encrypted: + data = self._aes.decrypt(data)[16:] + data = data[:-ord(data[-1])] + data = self.decompress(data) + return data + +def plugin_main(userkey, inpath, outpath): + rsa = RSA(userkey) + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = set(inf.namelist()) + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + return 1 + for name in META_NAMES: + namelist.remove(name) + try: + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('encryptedKey'),) + bookkey = ''.join(rights.findtext(expr)) + bookkey = rsa.decrypt(bookkey.decode('base64')) + # Padded as per RSAES-PKCS1-v1_5 + if bookkey[-17] != '\x00': + raise ADEPTError('problem decrypting session key') + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey[-16:], encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + zi = ZipInfo('mimetype', compress_type=ZIP_STORED) + outf.writestr(zi, inf.read('mimetype')) + for path in namelist: + data = inf.read(path) + outf.writestr(path, decryptor.decrypt(path, data)) + except: + return 2 + return 0 + +from calibre.customize import FileTypePlugin + +class IneptDeDRM(FileTypePlugin): + name = 'Inept Epub DeDRM' + description = 'Removes DRM from secure Adobe epub files. \ + Credit given to I <3 Cabbages for the original stand-alone scripts.' + supported_platforms = ['linux', 'osx', 'windows'] + author = 'DiapDealer' + version = (0, 1, 0) + minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions. + file_types = set(['epub']) + on_import = True + priority = 100 + + def run(self, path_to_ebook): + global AES + global RSA + + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + from calibre.constants import iswindows, isosx + + # Add the included pycrypto import directory for Windows users. + # Add the included Carbon import directory for Mac users. + pdir = 'windows' if iswindows else 'osx' if isosx else 'linux' + ppath = os.path.join(self.sys_insertion_path, pdir) + #sys.path.insert(0, ppath) + sys.path.append(ppath) + + AES, RSA = _load_crypto() + + if AES == None or RSA == None: + # Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.' + sys.path.remove(ppath) + raise ADEPTError('IneptEpub: Failed to load crypto libs... Adobe Epubs can\'t be decrypted.') + return + + # Load any keyfiles (*.der) included Calibre's config directory. + userkeys = [] + + # Find Calibre's configuration directory. + confpath = os.path.split(os.path.split(self.plugin_path)[0])[0] + print 'IneptEpub: Calibre configuration directory = %s' % confpath + files = os.listdir(confpath) + filefilter = re.compile("\.der$", re.IGNORECASE) + files = filter(filefilter.search, files) + + if files: + try: + for filename in files: + fpath = os.path.join(confpath, filename) + with open(fpath, 'rb') as f: + userkeys.append(f.read()) + print 'IneptEpub: Keyfile %s found in config folder.' % filename + except IOError: + print 'IneptEpub: Error reading keyfiles from config directory.' + pass + else: + # Try to find key from ADE install and save the key in + # Calibre's configuration directory for future use. + if iswindows or isosx: + # ADE key retrieval script included in respective OS folder. + from ade_key import retrieve_key + try: + keydata = retrieve_key() + userkeys.append(keydata) + keypath = os.path.join(confpath, 'adeptkey.der') + with open(keypath, 'wb') as f: + f.write(keydata) + print 'IneptEpub: Created keyfile from ADE install.' + except: + print 'IneptEpub: Couldn\'t Retrieve key from ADE install.' + pass + + if not userkeys: + # No user keys found... bail out. + sys.path.remove(ppath) + raise ADEPTError('IneptEpub - No keys found. Check keyfile(s)/ADE install') + return + + # Attempt to decrypt epub with each encryption key found. + for userkey in userkeys: + # Create a TemporaryPersistent file to work with. + of = self.temporary_file('.epub') + + # Give the user key, ebook and TemporaryPersistent file to the plugin_main function. + result = plugin_main(userkey, path_to_ebook, of.name) + + # Ebook is not an Adobe Adept epub... do nothing and pass it on. + # This allows a non-encrypted epub to be imported without error messages. + if result == 1: + print 'IneptEpub: Not an Adobe Adept Epub... punting.' + of.close() + sys.path.remove(ppath) + return path_to_ebook + break + + # Decryption was successful return the modified PersistentTemporary + # file to Calibre's import process. + if result == 0: + print 'IneptEpub: Encryption successfully removed.' + of.close + sys.path.remove(ppath) + return of.name + break + + print 'IneptEpub: Encryption key invalid... trying others.' + of.close() + + # Something went wrong with decryption. + # Import the original unmolested epub. + of.close + sys.path.remove(ppath) + raise ADEPTError('IneptEpub - Ultimately failed to decrypt') + return + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Cipher/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Cipher/__init__.py new file mode 100644 index 0000000..d8ceed9 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Cipher/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Secret-key encryption algorithms. + +Secret-key encryption algorithms transform plaintext in some way that +is dependent on a key, producing ciphertext. This transformation can +easily be reversed, if (and, hopefully, only if) one knows the key. + +The encryption modules here all support the interface described in PEP +272, "API for Block Encryption Algorithms". + +If you don't know which algorithm to choose, use AES because it's +standard and has undergone a fair bit of examination. + +Crypto.Cipher.AES Advanced Encryption Standard +Crypto.Cipher.ARC2 Alleged RC2 +Crypto.Cipher.ARC4 Alleged RC4 +Crypto.Cipher.Blowfish +Crypto.Cipher.CAST +Crypto.Cipher.DES The Data Encryption Standard. Very commonly used + in the past, but today its 56-bit keys are too small. +Crypto.Cipher.DES3 Triple DES. +Crypto.Cipher.XOR The simple XOR cipher. +""" + +__all__ = ['AES', 'ARC2', 'ARC4', + 'Blowfish', 'CAST', 'DES', 'DES3', + 'XOR' + ] + +__revision__ = "$Id$" + + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Hash/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Hash/__init__.py new file mode 100644 index 0000000..16dec73 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Hash/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Hashing algorithms + +Hash functions take arbitrary strings as input, and produce an output +of fixed size that is dependent on the input; it should never be +possible to derive the input data given only the hash function's +output. Hash functions can be used simply as a checksum, or, in +association with a public-key algorithm, can be used to implement +digital signatures. + +The hashing modules here all support the interface described in PEP +247, "API for Cryptographic Hash Functions". + +Submodules: +Crypto.Hash.HMAC RFC 2104: Keyed-Hashing for Message Authentication +Crypto.Hash.MD2 +Crypto.Hash.MD4 +Crypto.Hash.MD5 +Crypto.Hash.RIPEMD160 +Crypto.Hash.SHA +""" + +__all__ = ['HMAC', 'MD2', 'MD4', 'MD5', 'RIPEMD', 'RIPEMD160', 'SHA', 'SHA256'] +__revision__ = "$Id$" + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/RSA.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/RSA.py new file mode 100644 index 0000000..fd8678d --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/RSA.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# +# PublicKey/RSA.py : RSA public key primitive +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""RSA public-key cryptography algorithm.""" + +__revision__ = "$Id$" + +__all__ = ['generate', 'construct', 'error'] + +from Crypto.Util.python_compat import * + +from Crypto.PublicKey import _RSA, _slowmath, pubkey +from Crypto import Random + +try: + from Crypto.PublicKey import _fastmath +except ImportError: + _fastmath = None + +class _RSAobj(pubkey.pubkey): + keydata = ['n', 'e', 'd', 'p', 'q', 'u'] + + def __init__(self, implementation, key): + self.implementation = implementation + self.key = key + + def __getattr__(self, attrname): + if attrname in self.keydata: + # For backward compatibility, allow the user to get (not set) the + # RSA key parameters directly from this object. + return getattr(self.key, attrname) + else: + raise AttributeError("%s object has no %r attribute" % (self.__class__.__name__, attrname,)) + + def _encrypt(self, c, K): + return (self.key._encrypt(c),) + + def _decrypt(self, c): + #(ciphertext,) = c + (ciphertext,) = c[:1] # HACK - We should use the previous line + # instead, but this is more compatible and we're + # going to replace the Crypto.PublicKey API soon + # anyway. + return self.key._decrypt(ciphertext) + + def _blind(self, m, r): + return self.key._blind(m, r) + + def _unblind(self, m, r): + return self.key._unblind(m, r) + + def _sign(self, m, K=None): + return (self.key._sign(m),) + + def _verify(self, m, sig): + #(s,) = sig + (s,) = sig[:1] # HACK - We should use the previous line instead, but + # this is more compatible and we're going to replace + # the Crypto.PublicKey API soon anyway. + return self.key._verify(m, s) + + def has_private(self): + return self.key.has_private() + + def size(self): + return self.key.size() + + def can_blind(self): + return True + + def can_encrypt(self): + return True + + def can_sign(self): + return True + + def publickey(self): + return self.implementation.construct((self.key.n, self.key.e)) + + def __getstate__(self): + d = {} + for k in self.keydata: + try: + d[k] = getattr(self.key, k) + except AttributeError: + pass + return d + + def __setstate__(self, d): + if not hasattr(self, 'implementation'): + self.implementation = RSAImplementation() + t = [] + for k in self.keydata: + if not d.has_key(k): + break + t.append(d[k]) + self.key = self.implementation._math.rsa_construct(*tuple(t)) + + def __repr__(self): + attrs = [] + for k in self.keydata: + if k == 'n': + attrs.append("n(%d)" % (self.size()+1,)) + elif hasattr(self.key, k): + attrs.append(k) + if self.has_private(): + attrs.append("private") + return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs)) + +class RSAImplementation(object): + def __init__(self, **kwargs): + # 'use_fast_math' parameter: + # None (default) - Use fast math if available; Use slow math if not. + # True - Use fast math, and raise RuntimeError if it's not available. + # False - Use slow math. + use_fast_math = kwargs.get('use_fast_math', None) + if use_fast_math is None: # Automatic + if _fastmath is not None: + self._math = _fastmath + else: + self._math = _slowmath + + elif use_fast_math: # Explicitly select fast math + if _fastmath is not None: + self._math = _fastmath + else: + raise RuntimeError("fast math module not available") + + else: # Explicitly select slow math + self._math = _slowmath + + self.error = self._math.error + + # 'default_randfunc' parameter: + # None (default) - use Random.new().read + # not None - use the specified function + self._default_randfunc = kwargs.get('default_randfunc', None) + self._current_randfunc = None + + def _get_randfunc(self, randfunc): + if randfunc is not None: + return randfunc + elif self._current_randfunc is None: + self._current_randfunc = Random.new().read + return self._current_randfunc + + def generate(self, bits, randfunc=None, progress_func=None): + rf = self._get_randfunc(randfunc) + obj = _RSA.generate_py(bits, rf, progress_func) # TODO: Don't use legacy _RSA module + key = self._math.rsa_construct(obj.n, obj.e, obj.d, obj.p, obj.q, obj.u) + return _RSAobj(self, key) + + def construct(self, tup): + key = self._math.rsa_construct(*tup) + return _RSAobj(self, key) + +_impl = RSAImplementation() +generate = _impl.generate +construct = _impl.construct +error = _impl.error + +# vim:set ts=4 sw=4 sts=4 expandtab: + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_RSA.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_RSA.py new file mode 100644 index 0000000..236031a --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_RSA.py @@ -0,0 +1,95 @@ +# +# RSA.py : RSA encryption/decryption +# +# Part of the Python Cryptography Toolkit +# +# Written by Andrew Kuchling, Paul Swartz, and others +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== +# + +__revision__ = "$Id$" + +from Crypto.PublicKey import pubkey +from Crypto.Util import number + +def generate_py(bits, randfunc, progress_func=None): + """generate(bits:int, randfunc:callable, progress_func:callable) + + Generate an RSA key of length 'bits', using 'randfunc' to get + random data and 'progress_func', if present, to display + the progress of the key generation. + """ + obj=RSAobj() + obj.e = 65537L + + # Generate the prime factors of n + if progress_func: + progress_func('p,q\n') + p = q = 1L + while number.size(p*q) < bits: + # Note that q might be one bit longer than p if somebody specifies an odd + # number of bits for the key. (Why would anyone do that? You don't get + # more security.) + # + # Note also that we ensure that e is coprime to (p-1) and (q-1). + # This is needed for encryption to work properly, according to the 1997 + # paper by Robert D. Silverman of RSA Labs, "Fast generation of random, + # strong RSA primes", available at + # http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.17.2713&rep=rep1&type=pdf + # Since e=65537 is prime, it is sufficient to check that e divides + # neither (p-1) nor (q-1). + p = 1L + while (p - 1) % obj.e == 0: + if progress_func: + progress_func('p\n') + p = pubkey.getPrime(bits/2, randfunc) + q = 1L + while (q - 1) % obj.e == 0: + if progress_func: + progress_func('q\n') + q = pubkey.getPrime(bits - (bits/2), randfunc) + + # p shall be smaller than q (for calc of u) + if p > q: + (p, q)=(q, p) + obj.p = p + obj.q = q + + if progress_func: + progress_func('u\n') + obj.u = pubkey.inverse(obj.p, obj.q) + obj.n = obj.p*obj.q + + if progress_func: + progress_func('d\n') + obj.d=pubkey.inverse(obj.e, (obj.p-1)*(obj.q-1)) + + assert bits <= 1+obj.size(), "Generated key is too small" + + return obj + +class RSAobj(pubkey.pubkey): + + def size(self): + """size() : int + Return the maximum number of bits that can be handled by this key. + """ + return number.size(self.n) - 1 + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/__init__.py new file mode 100644 index 0000000..68e1d88 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Public-key encryption and signature algorithms. + +Public-key encryption uses two different keys, one for encryption and +one for decryption. The encryption key can be made public, and the +decryption key is kept private. Many public-key algorithms can also +be used to sign messages, and some can *only* be used for signatures. + +Crypto.PublicKey.DSA Digital Signature Algorithm. (Signature only) +Crypto.PublicKey.ElGamal (Signing and encryption) +Crypto.PublicKey.RSA (Signing, encryption, and blinding) +Crypto.PublicKey.qNEW (Signature only) + +""" + +__all__ = ['RSA', 'DSA', 'ElGamal', 'qNEW'] +__revision__ = "$Id$" + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_slowmath.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_slowmath.py new file mode 100644 index 0000000..05f1e18 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/_slowmath.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# PubKey/RSA/_slowmath.py : Pure Python implementation of the RSA portions of _fastmath +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Pure Python implementation of the RSA-related portions of Crypto.PublicKey._fastmath.""" + +__revision__ = "$Id$" + +__all__ = ['rsa_construct'] + +from Crypto.Util.python_compat import * + +from Crypto.Util.number import size, inverse + +class error(Exception): + pass + +class _RSAKey(object): + def _blind(self, m, r): + # compute r**e * m (mod n) + return m * pow(r, self.e, self.n) + + def _unblind(self, m, r): + # compute m / r (mod n) + return inverse(r, self.n) * m % self.n + + def _decrypt(self, c): + # compute c**d (mod n) + if not self.has_private(): + raise TypeError("No private key") + return pow(c, self.d, self.n) # TODO: CRT exponentiation + + def _encrypt(self, m): + # compute m**d (mod n) + return pow(m, self.e, self.n) + + def _sign(self, m): # alias for _decrypt + if not self.has_private(): + raise TypeError("No private key") + return self._decrypt(m) + + def _verify(self, m, sig): + return self._encrypt(sig) == m + + def has_private(self): + return hasattr(self, 'd') + + def size(self): + """Return the maximum number of bits that can be encrypted""" + return size(self.n) - 1 + +def rsa_construct(n, e, d=None, p=None, q=None, u=None): + """Construct an RSAKey object""" + assert isinstance(n, long) + assert isinstance(e, long) + assert isinstance(d, (long, type(None))) + assert isinstance(p, (long, type(None))) + assert isinstance(q, (long, type(None))) + assert isinstance(u, (long, type(None))) + obj = _RSAKey() + obj.n = n + obj.e = e + if d is not None: obj.d = d + if p is not None: obj.p = p + if q is not None: obj.q = q + if u is not None: obj.u = u + return obj + +class _DSAKey(object): + def size(self): + """Return the maximum number of bits that can be encrypted""" + return size(self.p) - 1 + + def has_private(self): + return hasattr(self, 'x') + + def _sign(self, m, k): # alias for _decrypt + # SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API. + if not self.has_private(): + raise TypeError("No private key") + if not (1L < k < self.q): + raise ValueError("k is not between 2 and q-1") + inv_k = inverse(k, self.q) # Compute k**-1 mod q + r = pow(self.g, k, self.p) % self.q # r = (g**k mod p) mod q + s = (inv_k * (m + self.x * r)) % self.q + return (r, s) + + def _verify(self, m, r, s): + # SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API. + if not (0 < r < self.q) or not (0 < s < self.q): + return False + w = inverse(s, self.q) + u1 = (m*w) % self.q + u2 = (r*w) % self.q + v = (pow(self.g, u1, self.p) * pow(self.y, u2, self.p) % self.p) % self.q + return v == r + +def dsa_construct(y, g, p, q, x=None): + assert isinstance(y, long) + assert isinstance(g, long) + assert isinstance(p, long) + assert isinstance(q, long) + assert isinstance(x, (long, type(None))) + obj = _DSAKey() + obj.y = y + obj.g = g + obj.p = p + obj.q = q + if x is not None: obj.x = x + return obj + + +# vim:set ts=4 sw=4 sts=4 expandtab: + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/pubkey.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/pubkey.py new file mode 100644 index 0000000..90f4603 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/PublicKey/pubkey.py @@ -0,0 +1,192 @@ +# +# pubkey.py : Internal functions for public key operations +# +# Part of the Python Cryptography Toolkit +# +# Written by Andrew Kuchling, Paul Swartz, and others +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== +# + +__revision__ = "$Id$" + +import types, warnings +from Crypto.Util.number import * + +# Basic public key class +class pubkey: + def __init__(self): + pass + + def __getstate__(self): + """To keep key objects platform-independent, the key data is + converted to standard Python long integers before being + written out. It will then be reconverted as necessary on + restoration.""" + d=self.__dict__ + for key in self.keydata: + if d.has_key(key): d[key]=long(d[key]) + return d + + def __setstate__(self, d): + """On unpickling a key object, the key data is converted to the big +number representation being used, whether that is Python long +integers, MPZ objects, or whatever.""" + for key in self.keydata: + if d.has_key(key): self.__dict__[key]=bignum(d[key]) + + def encrypt(self, plaintext, K): + """encrypt(plaintext:string|long, K:string|long) : tuple + Encrypt the string or integer plaintext. K is a random + parameter required by some algorithms. + """ + wasString=0 + if isinstance(plaintext, types.StringType): + plaintext=bytes_to_long(plaintext) ; wasString=1 + if isinstance(K, types.StringType): + K=bytes_to_long(K) + ciphertext=self._encrypt(plaintext, K) + if wasString: return tuple(map(long_to_bytes, ciphertext)) + else: return ciphertext + + def decrypt(self, ciphertext): + """decrypt(ciphertext:tuple|string|long): string + Decrypt 'ciphertext' using this key. + """ + wasString=0 + if not isinstance(ciphertext, types.TupleType): + ciphertext=(ciphertext,) + if isinstance(ciphertext[0], types.StringType): + ciphertext=tuple(map(bytes_to_long, ciphertext)) ; wasString=1 + plaintext=self._decrypt(ciphertext) + if wasString: return long_to_bytes(plaintext) + else: return plaintext + + def sign(self, M, K): + """sign(M : string|long, K:string|long) : tuple + Return a tuple containing the signature for the message M. + K is a random parameter required by some algorithms. + """ + if (not self.has_private()): + raise TypeError('Private key not available in this object') + if isinstance(M, types.StringType): M=bytes_to_long(M) + if isinstance(K, types.StringType): K=bytes_to_long(K) + return self._sign(M, K) + + def verify (self, M, signature): + """verify(M:string|long, signature:tuple) : bool + Verify that the signature is valid for the message M; + returns true if the signature checks out. + """ + if isinstance(M, types.StringType): M=bytes_to_long(M) + return self._verify(M, signature) + + # alias to compensate for the old validate() name + def validate (self, M, signature): + warnings.warn("validate() method name is obsolete; use verify()", + DeprecationWarning) + + def blind(self, M, B): + """blind(M : string|long, B : string|long) : string|long + Blind message M using blinding factor B. + """ + wasString=0 + if isinstance(M, types.StringType): + M=bytes_to_long(M) ; wasString=1 + if isinstance(B, types.StringType): B=bytes_to_long(B) + blindedmessage=self._blind(M, B) + if wasString: return long_to_bytes(blindedmessage) + else: return blindedmessage + + def unblind(self, M, B): + """unblind(M : string|long, B : string|long) : string|long + Unblind message M using blinding factor B. + """ + wasString=0 + if isinstance(M, types.StringType): + M=bytes_to_long(M) ; wasString=1 + if isinstance(B, types.StringType): B=bytes_to_long(B) + unblindedmessage=self._unblind(M, B) + if wasString: return long_to_bytes(unblindedmessage) + else: return unblindedmessage + + + # The following methods will usually be left alone, except for + # signature-only algorithms. They both return Boolean values + # recording whether this key's algorithm can sign and encrypt. + def can_sign (self): + """can_sign() : bool + Return a Boolean value recording whether this algorithm can + generate signatures. (This does not imply that this + particular key object has the private information required to + to generate a signature.) + """ + return 1 + + def can_encrypt (self): + """can_encrypt() : bool + Return a Boolean value recording whether this algorithm can + encrypt data. (This does not imply that this + particular key object has the private information required to + to decrypt a message.) + """ + return 1 + + def can_blind (self): + """can_blind() : bool + Return a Boolean value recording whether this algorithm can + blind data. (This does not imply that this + particular key object has the private information required to + to blind a message.) + """ + return 0 + + # The following methods will certainly be overridden by + # subclasses. + + def size (self): + """size() : int + Return the maximum number of bits that can be handled by this key. + """ + return 0 + + def has_private (self): + """has_private() : bool + Return a Boolean denoting whether the object contains + private components. + """ + return 0 + + def publickey (self): + """publickey(): object + Return a new key object containing only the public information. + """ + return self + + def __eq__ (self, other): + """__eq__(other): 0, 1 + Compare us to other for equality. + """ + return self.__getstate__() == other.__getstate__() + + def __ne__ (self, other): + """__ne__(other): 0, 1 + Compare us to other for inequality. + """ + return not self.__eq__(other) diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaAccumulator.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaAccumulator.py new file mode 100644 index 0000000..3d35c23 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaAccumulator.py @@ -0,0 +1,139 @@ +# -*- coding: ascii -*- +# +# FortunaAccumulator.py : Fortuna's internal accumulator +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" + +from Crypto.Util.python_compat import * + +from binascii import b2a_hex +import time +import warnings + +from Crypto.pct_warnings import ClockRewindWarning +import SHAd256 + +import FortunaGenerator + +class FortunaPool(object): + """Fortuna pool type + + This object acts like a hash object, with the following differences: + + - It keeps a count (the .length attribute) of the number of bytes that + have been added to the pool + - It supports a .reset() method for in-place reinitialization + - The method to add bytes to the pool is .append(), not .update(). + """ + + digest_size = SHAd256.digest_size + + def __init__(self): + self.reset() + + def append(self, data): + self._h.update(data) + self.length += len(data) + + def digest(self): + return self._h.digest() + + def hexdigest(self): + return b2a_hex(self.digest()) + + def reset(self): + self._h = SHAd256.new() + self.length = 0 + +def which_pools(r): + """Return a list of pools indexes (in range(32)) that are to be included during reseed number r. + + According to _Practical Cryptography_, chapter 10.5.2 "Pools": + + "Pool P_i is included if 2**i is a divisor of r. Thus P_0 is used + every reseed, P_1 every other reseed, P_2 every fourth reseed, etc." + """ + # This is a separate function so that it can be unit-tested. + assert r >= 1 + retval = [] + mask = 0 + for i in range(32): + # "Pool P_i is included if 2**i is a divisor of [reseed_count]" + if (r & mask) == 0: + retval.append(i) + else: + break # optimization. once this fails, it always fails + mask = (mask << 1) | 1L + return retval + +class FortunaAccumulator(object): + + min_pool_size = 64 # TODO: explain why + reseed_interval = 0.100 # 100 ms TODO: explain why + + def __init__(self): + self.reseed_count = 0 + self.generator = FortunaGenerator.AESGenerator() + self.last_reseed = None + + # Initialize 32 FortunaPool instances. + # NB: This is _not_ equivalent to [FortunaPool()]*32, which would give + # us 32 references to the _same_ FortunaPool instance (and cause the + # assertion below to fail). + self.pools = [FortunaPool() for i in range(32)] # 32 pools + assert(self.pools[0] is not self.pools[1]) + + def random_data(self, bytes): + current_time = time.time() + if self.last_reseed > current_time: + warnings.warn("Clock rewind detected. Resetting last_reseed.", ClockRewindWarning) + self.last_reseed = None + if (self.pools[0].length >= self.min_pool_size and + (self.last_reseed is None or + current_time > self.last_reseed + self.reseed_interval)): + self._reseed(current_time) + # The following should fail if we haven't seeded the pool yet. + return self.generator.pseudo_random_data(bytes) + + def _reseed(self, current_time=None): + if current_time is None: + current_time = time.time() + seed = [] + self.reseed_count += 1 + self.last_reseed = current_time + for i in which_pools(self.reseed_count): + seed.append(self.pools[i].digest()) + self.pools[i].reset() + + seed = "".join(seed) + self.generator.reseed(seed) + + def add_random_event(self, source_number, pool_number, data): + assert 1 <= len(data) <= 32 + assert 0 <= source_number <= 255 + assert 0 <= pool_number <= 31 + self.pools[pool_number].append(chr(source_number)) + self.pools[pool_number].append(chr(len(data))) + self.pools[pool_number].append(data) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaGenerator.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaGenerator.py new file mode 100644 index 0000000..69733da --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/FortunaGenerator.py @@ -0,0 +1,128 @@ +# -*- coding: ascii -*- +# +# FortunaGenerator.py : Fortuna's internal PRNG +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" + +from Crypto.Util.python_compat import * + +import struct + +from Crypto.Util.number import ceil_shift, exact_log2, exact_div +from Crypto.Util import Counter +from Crypto.Cipher import AES + +import SHAd256 + +class AESGenerator(object): + """The Fortuna "generator" + + This is used internally by the Fortuna PRNG to generate arbitrary amounts + of pseudorandom data from a smaller amount of seed data. + + The output is generated by running AES-256 in counter mode and re-keying + after every mebibyte (2**16 blocks) of output. + """ + + block_size = AES.block_size # output block size in octets (128 bits) + key_size = 32 # key size in octets (256 bits) + + # Because of the birthday paradox, we expect to find approximately one + # collision for every 2**64 blocks of output from a real random source. + # However, this code generates pseudorandom data by running AES in + # counter mode, so there will be no collisions until the counter + # (theoretically) wraps around at 2**128 blocks. Thus, in order to prevent + # Fortuna's pseudorandom output from deviating perceptibly from a true + # random source, Ferguson and Schneier specify a limit of 2**16 blocks + # without rekeying. + max_blocks_per_request = 2**16 # Allow no more than this number of blocks per _pseudo_random_data request + + _four_kiblocks_of_zeros = "\0" * block_size * 4096 + + def __init__(self): + self.counter = Counter.new(nbits=self.block_size*8, initial_value=0, little_endian=True) + self.key = None + + # Set some helper constants + self.block_size_shift = exact_log2(self.block_size) + assert (1 << self.block_size_shift) == self.block_size + + self.blocks_per_key = exact_div(self.key_size, self.block_size) + assert self.key_size == self.blocks_per_key * self.block_size + + self.max_bytes_per_request = self.max_blocks_per_request * self.block_size + + def reseed(self, seed): + if self.key is None: + self.key = "\0" * self.key_size + self._set_key(SHAd256.new(self.key + seed).digest()) + self.counter() # increment counter + assert len(self.key) == self.key_size + + def pseudo_random_data(self, bytes): + assert bytes >= 0 + + num_full_blocks = bytes >> 20 + remainder = bytes & ((1<<20)-1) + + retval = [] + for i in xrange(num_full_blocks): + retval.append(self._pseudo_random_data(1<<20)) + retval.append(self._pseudo_random_data(remainder)) + + return "".join(retval) + + def _set_key(self, key): + self.key = key + self._cipher = AES.new(key, AES.MODE_CTR, counter=self.counter) + + def _pseudo_random_data(self, bytes): + if not (0 <= bytes <= self.max_bytes_per_request): + raise AssertionError("You cannot ask for more than 1 MiB of data per request") + + num_blocks = ceil_shift(bytes, self.block_size_shift) # num_blocks = ceil(bytes / self.block_size) + + # Compute the output + retval = self._generate_blocks(num_blocks)[:bytes] + + # Switch to a new key to avoid later compromises of this output (i.e. + # state compromise extension attacks) + self._set_key(self._generate_blocks(self.blocks_per_key)) + + assert len(retval) == bytes + assert len(self.key) == self.key_size + + return retval + + def _generate_blocks(self, num_blocks): + if self.key is None: + raise AssertionError("generator must be seeded before use") + assert 0 <= num_blocks <= self.max_blocks_per_request + retval = [] + for i in xrange(num_blocks >> 12): # xrange(num_blocks / 4096) + retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros)) + remaining_bytes = (num_blocks & 4095) << self.block_size_shift # (num_blocks % 4095) * self.block_size + retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros[:remaining_bytes])) + return "".join(retval) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/SHAd256.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/SHAd256.py new file mode 100644 index 0000000..288a53e --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/SHAd256.py @@ -0,0 +1,88 @@ +# -*- coding: ascii -*- +# +# Random/Fortuna/SHAd256.py : SHA_d-256 hash function implementation +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""\ +SHA_d-256 hash function implementation. + +This module should comply with PEP 247. +""" + +__revision__ = "$Id$" +__all__ = ['new', 'digest_size'] + +from Crypto.Util.python_compat import * + +from binascii import b2a_hex + +from Crypto.Hash import SHA256 + +assert SHA256.digest_size == 32 + +class _SHAd256(object): + """SHA-256, doubled. + + Returns SHA-256(SHA-256(data)). + """ + + digest_size = SHA256.digest_size + + _internal = object() + + def __init__(self, internal_api_check, sha256_hash_obj): + if internal_api_check is not self._internal: + raise AssertionError("Do not instantiate this class directly. Use %s.new()" % (__name__,)) + self._h = sha256_hash_obj + + # PEP 247 "copy" method + def copy(self): + """Return a copy of this hashing object""" + return _SHAd256(SHAd256._internal, self._h.copy()) + + # PEP 247 "digest" method + def digest(self): + """Return the hash value of this object as a binary string""" + retval = SHA256.new(self._h.digest()).digest() + assert len(retval) == 32 + return retval + + # PEP 247 "hexdigest" method + def hexdigest(self): + """Return the hash value of this object as a (lowercase) hexadecimal string""" + retval = b2a_hex(self.digest()) + assert len(retval) == 64 + return retval + + # PEP 247 "update" method + def update(self, data): + self._h.update(data) + +# PEP 247 module-level "digest_size" variable +digest_size = _SHAd256.digest_size + +# PEP 247 module-level "new" function +def new(data=""): + """Return a new SHAd256 hashing object""" + return _SHAd256(_SHAd256._internal, SHA256.new(data)) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/Fortuna/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/__init__.py new file mode 100644 index 0000000..2fbbecb --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/__init__.py @@ -0,0 +1,40 @@ +# +# Random/OSRNG/__init__.py : Platform-independent OS RNG API +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Provides a platform-independent interface to the random number generators +supplied by various operating systems.""" + +__revision__ = "$Id$" + +import os + +if os.name == 'posix': + from Crypto.Random.OSRNG.posix import new +elif os.name == 'nt': + from Crypto.Random.OSRNG.nt import new +elif hasattr(os, 'urandom'): + from Crypto.Random.OSRNG.fallback import new +else: + raise ImportError("Not implemented") + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/fallback.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/fallback.py new file mode 100644 index 0000000..5bb6126 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/fallback.py @@ -0,0 +1,46 @@ +# +# Random/OSRNG/fallback.py : Fallback entropy source for systems with os.urandom +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + + +__revision__ = "$Id$" +__all__ = ['PythonOSURandomRNG'] + +import os + +from rng_base import BaseRNG + +class PythonOSURandomRNG(BaseRNG): + + name = "" + + def __init__(self): + self._read = os.urandom + BaseRNG.__init__(self) + + def _close(self): + self._read = None + +def new(*args, **kwargs): + return PythonOSURandomRNG(*args, **kwargs) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/nt.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/nt.py new file mode 100644 index 0000000..c1c2f44 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/nt.py @@ -0,0 +1,74 @@ +# +# Random/OSRNG/nt.py : OS entropy source for MS Windows +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + + +__revision__ = "$Id$" +__all__ = ['WindowsRNG'] + +import winrandom +from rng_base import BaseRNG + +class WindowsRNG(BaseRNG): + + name = "" + + def __init__(self): + self.__winrand = winrandom.new() + BaseRNG.__init__(self) + + def flush(self): + """Work around weakness in Windows RNG. + + The CryptGenRandom mechanism in some versions of Windows allows an + attacker to learn 128 KiB of past and future output. As a workaround, + this function reads 128 KiB of 'random' data from Windows and discards + it. + + For more information about the weaknesses in CryptGenRandom, see + _Cryptanalysis of the Random Number Generator of the Windows Operating + System_, by Leo Dorrendorf and Zvi Gutterman and Benny Pinkas + http://eprint.iacr.org/2007/419 + """ + if self.closed: + raise ValueError("I/O operation on closed file") + data = self.__winrand.get_bytes(128*1024) + assert (len(data) == 128*1024) + BaseRNG.flush(self) + + def _close(self): + self.__winrand = None + + def _read(self, N): + # Unfortunately, research shows that CryptGenRandom doesn't provide + # forward secrecy and fails the next-bit test unless we apply a + # workaround, which we do here. See http://eprint.iacr.org/2007/419 + # for information on the vulnerability. + self.flush() + data = self.__winrand.get_bytes(N) + self.flush() + return data + +def new(*args, **kwargs): + return WindowsRNG(*args, **kwargs) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/rng_base.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/rng_base.py new file mode 100644 index 0000000..2f49019 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/OSRNG/rng_base.py @@ -0,0 +1,86 @@ +# +# Random/OSRNG/rng_base.py : Base class for OSRNG +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" + +from Crypto.Util.python_compat import * + +class BaseRNG(object): + + def __init__(self): + self.closed = False + self._selftest() + + def __del__(self): + self.close() + + def _selftest(self): + # Test that urandom can return data + data = self.read(16) + if len(data) != 16: + raise AssertionError("read truncated") + + # Test that we get different data every time (if we don't, the RNG is + # probably malfunctioning) + data2 = self.read(16) + if data == data2: + raise AssertionError("OS RNG returned duplicate data") + + # PEP 343: Support for the "with" statement + def __enter__(self): + pass + def __exit__(self): + """PEP 343 support""" + self.close() + + def close(self): + if not self.closed: + self._close() + self.closed = True + + def flush(self): + pass + + def read(self, N=-1): + """Return N bytes from the RNG.""" + if self.closed: + raise ValueError("I/O operation on closed file") + if not isinstance(N, (long, int)): + raise TypeError("an integer is required") + if N < 0: + raise ValueError("cannot read to end of infinite stream") + elif N == 0: + return "" + data = self._read(N) + if len(data) != N: + raise AssertionError("%s produced truncated output (requested %d, got %d)" % (self.name, N, len(data))) + return data + + def _close(self): + raise NotImplementedError("child class must implement this") + + def _read(self, N): + raise NotImplementedError("child class must implement this") + + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/_UserFriendlyRNG.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/_UserFriendlyRNG.py new file mode 100644 index 0000000..fd40e96 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/_UserFriendlyRNG.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# +# Random/_UserFriendlyRNG.py : A user-friendly random number generator +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" + +from Crypto.Util.python_compat import * + +import os +import threading +import struct +import time +from math import floor + +from Crypto.Random import OSRNG +from Crypto.Random.Fortuna import FortunaAccumulator + +class _EntropySource(object): + def __init__(self, accumulator, src_num): + self._fortuna = accumulator + self._src_num = src_num + self._pool_num = 0 + + def feed(self, data): + self._fortuna.add_random_event(self._src_num, self._pool_num, data) + self._pool_num = (self._pool_num + 1) & 31 + +class _EntropyCollector(object): + + def __init__(self, accumulator): + self._osrng = OSRNG.new() + self._osrng_es = _EntropySource(accumulator, 255) + self._time_es = _EntropySource(accumulator, 254) + self._clock_es = _EntropySource(accumulator, 253) + + def reinit(self): + # Add 256 bits to each of the 32 pools, twice. (For a total of 16384 + # bits collected from the operating system.) + for i in range(2): + block = self._osrng.read(32*32) + for p in range(32): + self._osrng_es.feed(block[p*32:(p+1)*32]) + block = None + self._osrng.flush() + + def collect(self): + # Collect 64 bits of entropy from the operating system and feed it to Fortuna. + self._osrng_es.feed(self._osrng.read(8)) + + # Add the fractional part of time.time() + t = time.time() + self._time_es.feed(struct.pack("@I", int(2**30 * (t - floor(t))))) + + # Add the fractional part of time.clock() + t = time.clock() + self._clock_es.feed(struct.pack("@I", int(2**30 * (t - floor(t))))) + + +class _UserFriendlyRNG(object): + + def __init__(self): + self.closed = False + self._fa = FortunaAccumulator.FortunaAccumulator() + self._ec = _EntropyCollector(self._fa) + self.reinit() + + def reinit(self): + """Initialize the random number generator and seed it with entropy from + the operating system. + """ + self._pid = os.getpid() + self._ec.reinit() + + def close(self): + self.closed = True + self._osrng = None + self._fa = None + + def flush(self): + pass + + def read(self, N): + """Return N bytes from the RNG.""" + if self.closed: + raise ValueError("I/O operation on closed file") + if not isinstance(N, (long, int)): + raise TypeError("an integer is required") + if N < 0: + raise ValueError("cannot read to end of infinite stream") + + # Collect some entropy and feed it to Fortuna + self._ec.collect() + + # Ask Fortuna to generate some bytes + retval = self._fa.random_data(N) + + # Check that we haven't forked in the meantime. (If we have, we don't + # want to use the data, because it might have been duplicated in the + # parent process. + self._check_pid() + + # Return the random data. + return retval + + def _check_pid(self): + # Lame fork detection to remind developers to invoke Random.atfork() + # after every call to os.fork(). Note that this check is not reliable, + # since process IDs can be reused on most operating systems. + # + # You need to do Random.atfork() in the child process after every call + # to os.fork() to avoid reusing PRNG state. If you want to avoid + # leaking PRNG state to child processes (for example, if you are using + # os.setuid()) then you should also invoke Random.atfork() in the + # *parent* process. + if os.getpid() != self._pid: + raise AssertionError("PID check failed. RNG must be re-initialized after fork(). Hint: Try Random.atfork()") + + +class _LockingUserFriendlyRNG(_UserFriendlyRNG): + def __init__(self): + self._lock = threading.Lock() + _UserFriendlyRNG.__init__(self) + + def close(self): + self._lock.acquire() + try: + return _UserFriendlyRNG.close(self) + finally: + self._lock.release() + + def reinit(self): + self._lock.acquire() + try: + return _UserFriendlyRNG.reinit(self) + finally: + self._lock.release() + + def read(self, bytes): + self._lock.acquire() + try: + return _UserFriendlyRNG.read(self, bytes) + finally: + self._lock.release() + +class RNGFile(object): + def __init__(self, singleton): + self.closed = False + self._singleton = singleton + + # PEP 343: Support for the "with" statement + def __enter__(self): + """PEP 343 support""" + def __exit__(self): + """PEP 343 support""" + self.close() + + def close(self): + # Don't actually close the singleton, just close this RNGFile instance. + self.closed = True + self._singleton = None + + def read(self, bytes): + if self.closed: + raise ValueError("I/O operation on closed file") + return self._singleton.read(bytes) + + def flush(self): + if self.closed: + raise ValueError("I/O operation on closed file") + +_singleton_lock = threading.Lock() +_singleton = None +def _get_singleton(): + global _singleton + _singleton_lock.acquire() + try: + if _singleton is None: + _singleton = _LockingUserFriendlyRNG() + return _singleton + finally: + _singleton_lock.release() + +def new(): + return RNGFile(_get_singleton()) + +def reinit(): + _get_singleton().reinit() + +def get_random_bytes(n): + """Return the specified number of cryptographically-strong random bytes.""" + return _get_singleton().read(n) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/__init__.py new file mode 100644 index 0000000..0d0e6f2 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Random/__init__.py : PyCrypto random number generation +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" +__all__ = ['new'] + +import OSRNG +import _UserFriendlyRNG + +def new(*args, **kwargs): + """Return a file-like object that outputs cryptographically random bytes.""" + return _UserFriendlyRNG.new(*args, **kwargs) + +def atfork(): + """Call this whenever you call os.fork()""" + _UserFriendlyRNG.reinit() + +def get_random_bytes(n): + """Return the specified number of cryptographically-strong random bytes.""" + return _UserFriendlyRNG.get_random_bytes(n) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/random.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/random.py new file mode 100644 index 0000000..e45a87b --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Random/random.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Random/random.py : Strong alternative for the standard 'random' module +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""A cryptographically strong version of Python's standard "random" module.""" + +__revision__ = "$Id$" +__all__ = ['StrongRandom', 'getrandbits', 'randrange', 'randint', 'choice', 'shuffle', 'sample'] + +from Crypto import Random + +from Crypto.Util.python_compat import * + +class StrongRandom(object): + def __init__(self, rng=None, randfunc=None): + if randfunc is None and rng is None: + self._randfunc = None + elif randfunc is not None and rng is None: + self._randfunc = randfunc + elif randfunc is None and rng is not None: + self._randfunc = rng.read + else: + raise ValueError("Cannot specify both 'rng' and 'randfunc'") + + def getrandbits(self, k): + """Return a python long integer with k random bits.""" + if self._randfunc is None: + self._randfunc = Random.new().read + mask = (1L << k) - 1 + return mask & bytes_to_long(self._randfunc(ceil_div(k, 8))) + + def randrange(self, *args): + """randrange([start,] stop[, step]): + Return a randomly-selected element from range(start, stop, step).""" + if len(args) == 3: + (start, stop, step) = args + elif len(args) == 2: + (start, stop) = args + step = 1 + elif len(args) == 1: + (stop,) = args + start = 0 + step = 1 + else: + raise TypeError("randrange expected at most 3 arguments, got %d" % (len(args),)) + if (not isinstance(start, (int, long)) + or not isinstance(stop, (int, long)) + or not isinstance(step, (int, long))): + raise TypeError("randrange requires integer arguments") + if step == 0: + raise ValueError("randrange step argument must not be zero") + + num_choices = ceil_div(stop - start, step) + if num_choices < 0: + num_choices = 0 + if num_choices < 1: + raise ValueError("empty range for randrange(%r, %r, %r)" % (start, stop, step)) + + # Pick a random number in the range of possible numbers + r = num_choices + while r >= num_choices: + r = self.getrandbits(size(num_choices)) + + return start + (step * r) + + def randint(self, a, b): + """Return a random integer N such that a <= N <= b.""" + if not isinstance(a, (int, long)) or not isinstance(b, (int, long)): + raise TypeError("randint requires integer arguments") + N = self.randrange(a, b+1) + assert a <= N <= b + return N + + def choice(self, seq): + """Return a random element from a (non-empty) sequence. + + If the seqence is empty, raises IndexError. + """ + if len(seq) == 0: + raise IndexError("empty sequence") + return seq[self.randrange(len(seq))] + + def shuffle(self, x): + """Shuffle the sequence in place.""" + # Make a (copy) of the list of objects we want to shuffle + items = list(x) + + # Choose a random item (without replacement) until all the items have been + # chosen. + for i in xrange(len(x)): + p = self.randint(len(items)) + x[i] = items[p] + del items[p] + + def sample(self, population, k): + """Return a k-length list of unique elements chosen from the population sequence.""" + + num_choices = len(population) + if k > num_choices: + raise ValueError("sample larger than population") + + retval = [] + selected = {} # we emulate a set using a dict here + for i in xrange(k): + r = None + while r is None or r in selected: + r = self.randrange(num_choices) + retval.append(population[r]) + selected[r] = 1 + return retval + +_r = StrongRandom() +getrandbits = _r.getrandbits +randrange = _r.randrange +randint = _r.randint +choice = _r.choice +shuffle = _r.shuffle +sample = _r.sample + +# These are at the bottom to avoid problems with recursive imports +from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes, size + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/Counter.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/Counter.py new file mode 100644 index 0000000..42dab42 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/Counter.py @@ -0,0 +1,61 @@ +# -*- coding: ascii -*- +# +# Util/Counter.py : Fast counter for use with CTR-mode ciphers +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +from Crypto.Util.python_compat import * + +from Crypto.Util import _counter +import struct + +# Factory function +def new(nbits, prefix="", suffix="", initial_value=1, overflow=0, little_endian=False, allow_wraparound=False, disable_shortcut=False): + # TODO: Document this + + # Sanity-check the message size + (nbytes, remainder) = divmod(nbits, 8) + if remainder != 0: + # In the future, we might support arbitrary bit lengths, but for now we don't. + raise ValueError("nbits must be a multiple of 8; got %d" % (nbits,)) + if nbytes < 1: + raise ValueError("nbits too small") + elif nbytes > 0xffff: + raise ValueError("nbits too large") + + initval = _encode(initial_value, nbytes, little_endian) + if little_endian: + return _counter._newLE(str(prefix), str(suffix), initval, allow_wraparound=allow_wraparound, disable_shortcut=disable_shortcut) + else: + return _counter._newBE(str(prefix), str(suffix), initval, allow_wraparound=allow_wraparound, disable_shortcut=disable_shortcut) + +def _encode(n, nbytes, little_endian=False): + retval = [] + n = long(n) + for i in range(nbytes): + if little_endian: + retval.append(chr(n & 0xff)) + else: + retval.insert(0, chr(n & 0xff)) + n >>= 8 + return "".join(retval) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/__init__.py new file mode 100644 index 0000000..aecd539 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Miscellaneous modules + +Contains useful modules that don't belong into any of the +other Crypto.* subpackages. + +Crypto.Util.number Number-theoretic functions (primality testing, etc.) +Crypto.Util.randpool Random number generation +Crypto.Util.RFC1751 Converts between 128-bit keys and human-readable + strings of words. + +""" + +__all__ = ['randpool', 'RFC1751', 'number', 'strxor'] + +__revision__ = "$Id$" + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/_number_new.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/_number_new.py new file mode 100644 index 0000000..2640392 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/_number_new.py @@ -0,0 +1,117 @@ +# -*- coding: ascii -*- +# +# Util/_number_new.py : utility functions +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +## NOTE: Do not import this module directly. Import these functions from Crypto.Util.number. + +__revision__ = "$Id$" +__all__ = ['ceil_shift', 'ceil_div', 'floor_div', 'exact_log2', 'exact_div'] + +from Crypto.Util.python_compat import * + +def ceil_shift(n, b): + """Return ceil(n / 2**b) without performing any floating-point or division operations. + + This is done by right-shifting n by b bits and incrementing the result by 1 + if any '1' bits were shifted out. + """ + if not isinstance(n, (int, long)) or not isinstance(b, (int, long)): + raise TypeError("unsupported operand type(s): %r and %r" % (type(n).__name__, type(b).__name__)) + + assert n >= 0 and b >= 0 # I haven't tested or even thought about negative values + mask = (1L << b) - 1 + if n & mask: + return (n >> b) + 1 + else: + return n >> b + +def ceil_div(a, b): + """Return ceil(a / b) without performing any floating-point operations.""" + + if not isinstance(a, (int, long)) or not isinstance(b, (int, long)): + raise TypeError("unsupported operand type(s): %r and %r" % (type(a).__name__, type(b).__name__)) + + (q, r) = divmod(a, b) + if r: + return q + 1 + else: + return q + +def floor_div(a, b): + if not isinstance(a, (int, long)) or not isinstance(b, (int, long)): + raise TypeError("unsupported operand type(s): %r and %r" % (type(a).__name__, type(b).__name__)) + + (q, r) = divmod(a, b) + return q + +def exact_log2(num): + """Find and return an integer i >= 0 such that num == 2**i. + + If no such integer exists, this function raises ValueError. + """ + + if not isinstance(num, (int, long)): + raise TypeError("unsupported operand type: %r" % (type(num).__name__,)) + + n = long(num) + if n <= 0: + raise ValueError("cannot compute logarithm of non-positive number") + + i = 0 + while n != 0: + if (n & 1) and n != 1: + raise ValueError("No solution could be found") + i += 1 + n >>= 1 + i -= 1 + + assert num == (1L << i) + return i + +def exact_div(p, d, allow_divzero=False): + """Find and return an integer n such that p == n * d + + If no such integer exists, this function raises ValueError. + + Both operands must be integers. + + If the second operand is zero, this function will raise ZeroDivisionError + unless allow_divzero is true (default: False). + """ + + if not isinstance(p, (int, long)) or not isinstance(d, (int, long)): + raise TypeError("unsupported operand type(s): %r and %r" % (type(p).__name__, type(d).__name__)) + + if d == 0 and allow_divzero: + n = 0 + if p != n * d: + raise ValueError("No solution could be found") + else: + (n, r) = divmod(p, d) + if r != 0: + raise ValueError("No solution could be found") + + assert p == n * d + return n + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/number.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/number.py new file mode 100644 index 0000000..4a71eae --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/number.py @@ -0,0 +1,250 @@ +# +# number.py : Number-theoretic functions +# +# Part of the Python Cryptography Toolkit +# +# Written by Andrew M. Kuchling, Barry A. Warsaw, and others +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== +# + +__revision__ = "$Id$" + +bignum = long +try: + from Crypto.PublicKey import _fastmath +except ImportError: + _fastmath = None + +# New functions +from _number_new import * + +# Commented out and replaced with faster versions below +## def long2str(n): +## s='' +## while n>0: +## s=chr(n & 255)+s +## n=n>>8 +## return s + +## import types +## def str2long(s): +## if type(s)!=types.StringType: return s # Integers will be left alone +## return reduce(lambda x,y : x*256+ord(y), s, 0L) + +def size (N): + """size(N:long) : int + Returns the size of the number N in bits. + """ + bits, power = 0,1L + while N >= power: + bits += 1 + power = power << 1 + return bits + +def getRandomNumber(N, randfunc=None): + """getRandomNumber(N:int, randfunc:callable):long + Return a random N-bit number. + + If randfunc is omitted, then Random.new().read is used. + + NOTE: Confusingly, this function does NOT return N random bits; It returns + a random N-bit number, i.e. a random number between 2**(N-1) and (2**N)-1. + + This function is for internal use only and may be renamed or removed in + the future. + """ + if randfunc is None: + _import_Random() + randfunc = Random.new().read + + S = randfunc(N/8) + odd_bits = N % 8 + if odd_bits != 0: + char = ord(randfunc(1)) >> (8-odd_bits) + S = chr(char) + S + value = bytes_to_long(S) + value |= 2L ** (N-1) # Ensure high bit is set + assert size(value) >= N + return value + +def GCD(x,y): + """GCD(x:long, y:long): long + Return the GCD of x and y. + """ + x = abs(x) ; y = abs(y) + while x > 0: + x, y = y % x, x + return y + +def inverse(u, v): + """inverse(u:long, u:long):long + Return the inverse of u mod v. + """ + u3, v3 = long(u), long(v) + u1, v1 = 1L, 0L + while v3 > 0: + q=u3 / v3 + u1, v1 = v1, u1 - v1*q + u3, v3 = v3, u3 - v3*q + while u1<0: + u1 = u1 + v + return u1 + +# Given a number of bits to generate and a random generation function, +# find a prime number of the appropriate size. + +def getPrime(N, randfunc=None): + """getPrime(N:int, randfunc:callable):long + Return a random N-bit prime number. + + If randfunc is omitted, then Random.new().read is used. + """ + if randfunc is None: + _import_Random() + randfunc = Random.new().read + + number=getRandomNumber(N, randfunc) | 1 + while (not isPrime(number, randfunc=randfunc)): + number=number+2 + return number + +def isPrime(N, randfunc=None): + """isPrime(N:long, randfunc:callable):bool + Return true if N is prime. + + If randfunc is omitted, then Random.new().read is used. + """ + _import_Random() + if randfunc is None: + randfunc = Random.new().read + + randint = StrongRandom(randfunc=randfunc).randint + + if N == 1: + return 0 + if N in sieve: + return 1 + for i in sieve: + if (N % i)==0: + return 0 + + # Use the accelerator if available + if _fastmath is not None: + return _fastmath.isPrime(N) + + # Compute the highest bit that's set in N + N1 = N - 1L + n = 1L + while (n> 1L + + # Rabin-Miller test + for c in sieve[:7]: + a=long(c) ; d=1L ; t=n + while (t): # Iterate over the bits in N1 + x=(d*d) % N + if x==1L and d!=1L and d!=N1: + return 0 # Square root of 1 found + if N1 & t: + d=(x*a) % N + else: + d=x + t = t >> 1L + if d!=1L: + return 0 + return 1 + +# Small primes used for checking primality; these are all the primes +# less than 256. This should be enough to eliminate most of the odd +# numbers before needing to do a Rabin-Miller test at all. + +sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, + 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, + 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, + 197, 199, 211, 223, 227, 229, 233, 239, 241, 251] + +# Improved conversion functions contributed by Barry Warsaw, after +# careful benchmarking + +import struct + +def long_to_bytes(n, blocksize=0): + """long_to_bytes(n:long, blocksize:int) : string + Convert a long integer to a byte string. + + If optional blocksize is given and greater than zero, pad the front of the + byte string with binary zeros so that the length is a multiple of + blocksize. + """ + # after much testing, this algorithm was deemed to be the fastest + s = '' + n = long(n) + pack = struct.pack + while n > 0: + s = pack('>I', n & 0xffffffffL) + s + n = n >> 32 + # strip off leading zeros + for i in range(len(s)): + if s[i] != '\000': + break + else: + # only happens when n == 0 + s = '\000' + i = 0 + s = s[i:] + # add back some pad bytes. this could be done more efficiently w.r.t. the + # de-padding being done above, but sigh... + if blocksize > 0 and len(s) % blocksize: + s = (blocksize - len(s) % blocksize) * '\000' + s + return s + +def bytes_to_long(s): + """bytes_to_long(string) : long + Convert a byte string to a long integer. + + This is (essentially) the inverse of long_to_bytes(). + """ + acc = 0L + unpack = struct.unpack + length = len(s) + if length % 4: + extra = (4 - length % 4) + s = '\000' * extra + s + length = length + extra + for i in range(0, length, 4): + acc = (acc << 32) + unpack('>I', s[i:i+4])[0] + return acc + +# For backwards compatibility... +import warnings +def long2str(n, blocksize=0): + warnings.warn("long2str() has been replaced by long_to_bytes()") + return long_to_bytes(n, blocksize) +def str2long(s): + warnings.warn("str2long() has been replaced by bytes_to_long()") + return bytes_to_long(s) + +def _import_Random(): + # This is called in a function instead of at the module level in order to avoid problems with recursive imports + global Random, StrongRandom + from Crypto import Random + from Crypto.Random.random import StrongRandom + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/python_compat.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/python_compat.py new file mode 100644 index 0000000..7eb2803 --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/Util/python_compat.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# Util/python_compat.py : Compatibility code for old versions of Python +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Compatibility code for old versions of Python + +Currently, this just defines: + - True and False + - object + - isinstance +""" + +__revision__ = "$Id$" +__all__ = [] + +import sys +import __builtin__ + +# 'True' and 'False' aren't defined in Python 2.1. Define them. +try: + True, False +except NameError: + (True, False) = (1, 0) + __all__ += ['True', 'False'] + +# New-style classes were introduced in Python 2.2. Defining "object" in Python +# 2.1 lets us use new-style classes in versions of Python that support them, +# while still maintaining backward compatibility with old-style classes +try: + object +except NameError: + class object: pass + __all__ += ['object'] + +# Starting with Python 2.2, isinstance allows a tuple for the second argument. +# Also, builtins like "tuple", "list", "str", "unicode", "int", and "long" +# became first-class types, rather than functions. We want to support +# constructs like: +# isinstance(x, (int, long)) +# So we hack it for Python 2.1. +try: + isinstance(5, (int, long)) +except TypeError: + __all__ += ['isinstance'] + _builtin_type_map = { + tuple: type(()), + list: type([]), + str: type(""), + unicode: type(u""), + int: type(0), + long: type(0L), + } + def isinstance(obj, t): + if not __builtin__.isinstance(t, type(())): + # t is not a tuple + return __builtin__.isinstance(obj, _builtin_type_map.get(t, t)) + else: + # t is a tuple + for typ in t: + if __builtin__.isinstance(obj, _builtin_type_map.get(typ, typ)): + return True + return False + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/__init__.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/__init__.py new file mode 100644 index 0000000..d596e4f --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +"""Python Cryptography Toolkit + +A collection of cryptographic modules implementing various algorithms +and protocols. + +Subpackages: +Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4) +Crypto.Hash Hashing algorithms (MD5, SHA, HMAC) +Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing + transform). This package does not contain any + network protocols. +Crypto.PublicKey Public-key encryption and signature algorithms + (RSA, DSA) +Crypto.Util Various useful modules and functions (long-to-string + conversion, random number generation, number + theoretic functions) +""" + +__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util'] + +__version__ = '2.3' # See also below and setup.py +__revision__ = "$Id$" + +# New software should look at this instead of at __version__ above. +version_info = (2, 1, 0, 'final', 0) # See also above and setup.py + diff --git a/Calibre_Plugins/ineptepub_plugin/windows/Crypto/pct_warnings.py b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/pct_warnings.py new file mode 100644 index 0000000..2a5716c --- /dev/null +++ b/Calibre_Plugins/ineptepub_plugin/windows/Crypto/pct_warnings.py @@ -0,0 +1,57 @@ +# -*- coding: ascii -*- +# +# pct_warnings.py : PyCrypto warnings file +# +# Written in 2008 by Dwayne C. Litzenberger +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +# +# Base classes. All our warnings inherit from one of these in order to allow +# the user to specifically filter them. +# + +class CryptoWarning(Warning): + """Base class for PyCrypto warnings""" + +class CryptoDeprecationWarning(DeprecationWarning, CryptoWarning): + """Base PyCrypto DeprecationWarning class""" + +class CryptoRuntimeWarning(RuntimeWarning, CryptoWarning): + """Base PyCrypto RuntimeWarning class""" + +# +# Warnings that we might actually use +# + +class RandomPool_DeprecationWarning(CryptoDeprecationWarning): + """Issued when Crypto.Util.randpool.RandomPool is instantiated.""" + +class ClockRewindWarning(CryptoRuntimeWarning): + """Warning for when the system clock moves backwards.""" + +class GetRandomNumber_DeprecationWarning(CryptoDeprecationWarning): + """Issued when Crypto.Util.number.getRandomNumber is invoked.""" + +# By default, we want this warning to be shown every time we compensate for +# clock rewinding. +import warnings as _warnings +_warnings.filterwarnings('always', category=ClockRewindWarning, append=1) + +# vim:set ts=4 sw=4 sts=4 expandtab: diff --git a/Kindle_4_Mac_Tools/K4Munswindle.pyw b/Kindle_4_Mac_Tools/K4Munswindle.pyw new file mode 100644 index 0000000..234f38f --- /dev/null +++ b/Kindle_4_Mac_Tools/K4Munswindle.pyw @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import sys +sys.path.append('lib') +import os, os.path, urllib +import subprocess +from subprocess import Popen, PIPE, STDOUT +import subasyncio +from subasyncio import Process +import Tkinter +import Tkconstants +import tkFileDialog +import tkMessageBox +from scrolltextwidget import ScrolledText +import binascii +import hashlib + + +# +# Returns the SHA1 digest of "message" +# +def SHA1(message): + ctx = hashlib.sha1() + ctx.update(message) + return ctx.hexdigest() + + +class MainDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + self.root = root + self.interval = 2000 + self.p2 = None + self.status = Tkinter.Label(self, text='Remove Encryption from Kindle for Mac Mobi eBook') + self.status.pack(fill=Tkconstants.X, expand=1) + body = Tkinter.Frame(self) + body.pack(fill=Tkconstants.X, expand=1) + sticky = Tkconstants.E + Tkconstants.W + body.grid_columnconfigure(1, weight=2) + + Tkinter.Label(body, text='Locate your Kindle Applications').grid(row=0, sticky=Tkconstants.E) + self.k4mpath = Tkinter.Entry(body, width=50) + self.k4mpath.grid(row=0, column=1, sticky=sticky) + self.appname = '/Applications/Kindle for Mac.app' + if not os.path.exists(self.appname): + self.appname = '/Applications/Kindle.app' + cwd = self.appname + cwd = cwd.encode('utf-8') + self.k4mpath.insert(0, cwd) + button = Tkinter.Button(body, text="...", command=self.get_k4mpath) + button.grid(row=0, column=2) + + Tkinter.Label(body, text='Directory for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E) + self.outpath = Tkinter.Entry(body, width=50) + self.outpath.grid(row=1, column=1, sticky=sticky) + desktoppath = os.getenv('HOME') + '/Desktop/' + desktoppath = desktoppath.encode('utf-8') + self.outpath.insert(0, desktoppath) + button = Tkinter.Button(body, text="...", command=self.get_outpath) + button.grid(row=1, column=2) + + msg1 = 'Conversion Log \n\n' + self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) + self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky) + self.stext.insert(Tkconstants.END,msg1) + + buttons = Tkinter.Frame(self) + buttons.pack() + self.sbotton = Tkinter.Button( + buttons, text="Start", width=10, command=self.convertit) + self.sbotton.pack(side=Tkconstants.LEFT) + + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + self.qbutton = Tkinter.Button( + buttons, text="Quit", width=10, command=self.quitting) + self.qbutton.pack(side=Tkconstants.RIGHT) + + # read from subprocess pipe without blocking + # invoked every interval via the widget "after" + # option being used, so need to reset it for the next time + def processPipe(self): + poll = self.p2.wait('nowait') + if poll != None: + text = self.p2.readerr() + text += self.p2.read() + msg = text + '\n\n' + 'Encryption successfully removed\n' + if poll != 0: + msg = text + '\n\n' + 'Error: Encryption Removal Failed\n' + self.showCmdOutput(msg) + self.p2 = None + self.sbotton.configure(state='normal') + return + text = self.p2.readerr() + text += self.p2.read() + self.showCmdOutput(text) + # make sure we get invoked again by event loop after interval + self.stext.after(self.interval,self.processPipe) + return + + # post output from subprocess in scrolled text widget + def showCmdOutput(self, msg): + if msg and msg !='': + msg = msg.encode('utf-8') + self.stext.insert(Tkconstants.END,msg) + self.stext.yview_pickplace(Tkconstants.END) + return + + # run as a subprocess via pipes and collect stdout + def mobirdr(self, infile, outfile, pidnum): + cmdline = 'python ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) + return p2 + + def get_k4mpath(self): + k4mpath = tkFileDialog.askopenfilename( + parent=None, title='Select Your Kindle Application', + defaultextension='.app', filetypes=[('Kindle for Mac Application', '.app')]) + + if k4mpath: + k4mpath = os.path.normpath(k4mpath) + self.k4mpath.delete(0, Tkconstants.END) + self.k4mpath.insert(0, k4mpath) + return + + def get_outpath(self): + cwd = os.getcwdu() + cwd = cwd.encode('utf-8') + outpath = tkFileDialog.askdirectory( + parent=None, title='Directory to Put Non-DRM eBook into', + initialdir=cwd, initialfile=None) + if outpath: + outpath = os.path.normpath(outpath) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def quitting(self): + # kill any still running subprocess + if self.p2 != None: + if (self.p2.wait('nowait') == None): + self.p2.terminate() + self.root.destroy() + + # run as a gdb subprocess via pipes and collect stdout + def gdbrdr(self, k4mappfile, gdbcmds): + cmdline = 'gdb -q -silent -readnow -batch -x ' + gdbcmds + ' "' + k4mappfile + '"' + cmdline = cmdline.encode(sys.getfilesystemencoding()) + p3 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) + poll = p3.wait('wait') + results = p3.read() + pidnum = 'NOTAPID+' + topazbook = 0 + bookpath = 'book not found' + # parse the gdb results to get the last pid and the last azw/prc file name in the gdb listing + reslst = results.split('\n') + cnt = len(reslst) + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('PID is ') + if pp == 0: + pidnum = resline[7:] + topazbook = 0 + if pp > 0: + pidnum = resline[13:] + topazbook = 1 + fp = resline.find('File is ') + if fp >= 0: + tp1 = resline.find('.azw') + tp2 = resline.find('.prc') + if tp1 >= 0 or tp2 >= 0: + bookpath = resline[8:] + # put code here to get pid and file name + return pidnum, bookpath, topazbook + + # convert from 8 digit PID to proper 10 digit PID + def checksumPid(self, s): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + crc = (~binascii.crc32(s,-1))&0xFFFFFFFF + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + return res + + # start the process + def convertit(self): + # dictionary of all known Kindle for Mac Binaries + sha1_app_digests = { + 'e197ed2171ceb44a35c24bd30263b7253331694f' : 'gdb_kindle_cmds_r1.txt', + '4f702436171f84acc13bdf9f94fae91525aecef5' : 'gdb_kindle_cmds_r2.txt', + 'no_sha1_digest_key_here_________________' : 'no_gdb_kindle_cmds.txt', + } + # now disable the button to prevent multiple launches + self.sbotton.configure(state='disabled') + + k4mpath = self.k4mpath.get() + outpath = self.outpath.get() + + # basic error checking + if not k4mpath or not os.path.exists(k4mpath): + self.status['text'] = 'Error: Specified Kindle for Mac Application does not exist' + self.sbotton.configure(state='normal') + return + if not outpath: + self.status['text'] = 'Error: No output directory specified' + self.sbotton.configure(state='normal') + return + if not os.path.isdir(outpath): + self.status['text'] = 'Error specified outputdirectory does not exist' + self.sbotton.configure(state='normal') + return + if not os.path.isfile('/usr/bin/gdb'): + self.status['text'] = 'Error: gdb does not exist, install the XCode Develoepr Tools' + self.sbotton.configure(state='normal') + return + + # now check if the K4M app bianry is known and if so which gdbcmds to use + binary_app_file = k4mpath + '/Contents/MacOS/Kindle for Mac' + if not os.path.exists(binary_app_file): + binary_app_file = k4mpath + '/Contents/MacOS/Kindle' + + digest = SHA1(file(binary_app_file, 'rb').read()) + # print digest + gdbcmds = None + if digest in sha1_app_digests: + gdbcmds = sha1_app_digests[digest] + else : + self.status['text'] = 'Error: Kindle Application does not match any known version, sha1sum is ' + digest + self.sbotton.configure(state='normal') + return + + # run Kindle for Mac in gdb to get what we need + (pidnum, bookpath, topazbook) = self.gdbrdr(k4mpath, gdbcmds) + + if topazbook == 1: + log = 'Warning: ' + bookpath + ' is a Topaz book\n' + log += '\n\n' + log += 'To convert this book please use the Topaz Tools\n' + log += 'With the 8 digit PID: "' + pidnum + '"\n' + log += '\n\n' + log = log.encode('utf-8') + self.stext.insert(Tkconstants.END,log) + return + + pidnum = self.checksumPid(pidnum) + + # default output file name to be input file name + '_nodrm.mobi' + initname = os.path.splitext(os.path.basename(bookpath))[0] + initname += '_nodrm.mobi' + outpath += '/' + initname + + log = 'Command = "python mobidedrm.py"\n' + log += 'Mobi Path = "'+ bookpath + '"\n' + log += 'Output file = "' + outpath + '"\n' + log += 'PID = "' + pidnum + '"\n' + log += '\n\n' + log += 'Please Wait ...\n\n' + log = log.encode('utf-8') + self.stext.insert(Tkconstants.END,log) + self.p2 = self.mobirdr(bookpath, outpath, pidnum) + + # python does not seem to allow you to create + # your own eventloop which every other gui does - strange + # so need to use the widget "after" command to force + # event loop to run non-gui events every interval + self.stext.after(self.interval,self.processPipe) + return + + +def main(argv=None): + root = Tkinter.Tk() + root.title('Kindle for Mac eBook Encryption Removal') + root.resizable(True, False) + root.minsize(300, 0) + MainDialog(root).pack(fill=Tkconstants.X, expand=1) + root.mainloop() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Kindle_4_Mac_Tools/README_K4Mac_Tools.txt b/Kindle_4_Mac_Tools/README_K4Mac_Tools.txt new file mode 100644 index 0000000..43b4664 --- /dev/null +++ b/Kindle_4_Mac_Tools/README_K4Mac_Tools.txt @@ -0,0 +1,71 @@ +Kindle_4_Mac_Tools version 1.2 + +Kindle_4_Mac_Tools_v1.2.zip +http://www.mediafire.com/?8nz9rkg6p9nq23r + + +New in this release: + - support to identify Topaz Books and print their PID + so that standard Topaz_Tools can be used later if + desired (only works with Kindle Version 1.2.0) + +Prerequisites: + + - Kindle for Mac.app Version 1.0.0 Beta 1 (27214) + (this is the original version) + + or + + Kindle.app Version 1.2.0 (30689) + (this is the current version at Amazon) + + + - XCode Developer Tools **must** be Installed + (see your latest Mac OS X Install Disk for the installer) + + +The directions for use are: + +1. double-click to unzip the Kindle_4_Mac_Tools_v1.2.zip + +2. open the Kindle_4_Mac_Tools folder + +3. double-click on K4Munswindle.pyw + +In the window that opens: + +– hit the first '...' button to locate your Kindle Application + if it is not in /Applications + +– hit the second '...' button to select an output directory + (defaults to your Desktop) + +– hit the 'Start' button + +After a short delay, your Kindle application should open up automagically + +In Kindle for Mac: + + - hit the “Home” button to go home. + + - double-click on ONE of your books. + This should open the book. + +Once the book you want is open + +- hit the “Home” button and then exit the Kindle for Mac application + +Once you have exited the Kindle for Mac application you should see one of the following: + + - If the book you selected was a Topaz Book: + + A Warning message will appear in the Conversion Log indicating + that the book you opened was Topaz, along with the 8 digit PID + needed to convert it using Topaz_Tools + + - If the book you selected was a Mobi book: + + MobiDeDRM will be automagically started in the Conversion Log + window and if successful you should find your decoded book in + the output directory. + diff --git a/Kindle_4_Mac_Tools/gdb_kindle_cmds_r1.txt b/Kindle_4_Mac_Tools/gdb_kindle_cmds_r1.txt new file mode 100644 index 0000000..dfcd5ab --- /dev/null +++ b/Kindle_4_Mac_Tools/gdb_kindle_cmds_r1.txt @@ -0,0 +1,13 @@ +set verbose 0 +break * 0x00b2a56a +commands 1 +printf "PID is %s\n", $edx +continue +end +break * 0x014ca2af +commands 2 +printf "File is %s\n", $eax +continue +end +condition 2 $eax != 0 +run diff --git a/Kindle_4_Mac_Tools/gdb_kindle_cmds_r2.txt b/Kindle_4_Mac_Tools/gdb_kindle_cmds_r2.txt new file mode 100644 index 0000000..d699d56 --- /dev/null +++ b/Kindle_4_Mac_Tools/gdb_kindle_cmds_r2.txt @@ -0,0 +1,18 @@ +set verbose 0 +break * 0x00e37be4 +commands 1 +printf "PID is %s\n", $eax +continue +end +break * 0x0142cd94 +commands 2 +printf "File is %s\n", $eax +continue +end +condition 2 $eax != 0 +break * 0x01009c88 +commands 3 +printf "TOPAZ PID is %s\n", *(long*)($esp+12) +continue +end +run diff --git a/Kindle_4_Mac_Tools/lib/mobidedrm.py b/Kindle_4_Mac_Tools/lib/mobidedrm.py new file mode 100644 index 0000000..07d5f6f --- /dev/null +++ b/Kindle_4_Mac_Tools/lib/mobidedrm.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# It can run standalone to convert files, or it can be installed as a +# plugin for Calibre (http://calibre-ebook.com/about) so that +# importing files with DRM 'Just Works'. +# +# To create a Calibre plugin, rename this file so that the filename +# ends in '_plugin.py', put it into a ZIP file and import that Calibre +# using its plugin configuration GUI. +# +# Changelog +# 0.01 - Initial version +# 0.02 - Huffdic compressed books were not properly decrypted +# 0.03 - Wasn't checking MOBI header length +# 0.04 - Wasn't sanity checking size of data record +# 0.05 - It seems that the extra data flags take two bytes not four +# 0.06 - And that low bit does mean something after all :-) +# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size +# 0.08 - ...and also not in Mobi header version < 6 +# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! +# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre +# import filter it works when importing unencrypted files. +# Also now handles encrypted files that don't need a specific PID. +# 0.11 - use autoflushed stdout and proper return values +# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors +# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace +# and extra blank lines, converted CR/LF pairs at ends of each line, +# and other cosmetic fixes. +# 0.14 - Working out when the extra data flags are present has been problematic +# Versions 7 through 9 have tried to tweak the conditions, but have been +# only partially successful. Closer examination of lots of sample +# files reveals that a confusin has arisen because trailing data entries +# are not encrypted, but it turns out that the multibyte entries +# in utf8 file are encrypted. (Although neither kind gets compressed.) +# This knowledge leads to a simplification of the test for the +# trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'hearbeat', and is also quicker for long files. +# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. + +__version__ = '0.16' + +import sys +import struct +import binascii + +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +class DrmException(Exception): + pass + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + +def checksumPid(s): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + crc = (~binascii.crc32(s,-1))&0xFFFFFFFF + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + return res + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Multibyte data, if present, is included in the encryption, so + # we do not need to check the low bit. + # if flags & 1: + # num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + +class DrmStripper: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pid): + pid = pid.ljust(16,'\0') + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + temp_key = PC1(keyvec1, pid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: + found_key = finalkey + break + if not found_key: + # Then try the default encoding that doesn't require a PID + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum: + found_key = finalkey + break + return found_key + + def __init__(self, data_file, pid): + if checksumPid(pid[0:-2]) != pid: + raise DrmException("invalid PID checksum") + pid = pid[0:-2] + + self.data_file = data_file + header = data_file[0:72] + if header[0x3C:0x3C+8] != 'BOOKMOBI': + raise DrmException("invalid file format") + self.num_sections, = struct.unpack('>H', data_file[76:78]) + + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + + sect = self.loadSection(0) + records, = struct.unpack('>H', sect[0x8:0x8+2]) + mobi_length, = struct.unpack('>L',sect[0x14:0x18]) + mobi_version, = struct.unpack('>L',sect[0x68:0x6C]) + extra_data_flags = 0 + print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length) + if (mobi_length >= 0xE4) and (mobi_version >= 5): + extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4]) + print "Extra Data Flags = %d" %extra_data_flags + + crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) + if crypto_type == 0: + print "This book is not encrypted." + else: + if crypto_type == 1: + raise DrmException("cannot decode Mobipocket encryption type 1") + if crypto_type != 2: + raise DrmException("unknown encryption type: %d" % crypto_type) + + # calculate the keys + drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("no PIDs found in this file") + found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) + if not found_key: + raise DrmException("no key found. maybe the PID is incorrect") + + # kill the drm keys + self.patchSection(0, "\0" * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print "Decrypting. Please wait . . .", + new_data = self.data_file[:self.sections[1][0]] + for i in xrange(1, records+1): + data = self.loadSection(i) + extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags) + if i%100 == 0: + print ".", + # print "record %d, extra_size %d" %(i,extra_size) + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done" + + def getResult(self): + return self.data_file + +if not __name__ == "__main__": + from calibre.customize import FileTypePlugin + + class MobiDeDRM(FileTypePlugin): + name = 'MobiDeDRM' # Name of the plugin + description = 'Removes DRM from secure Mobi files' + supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on + author = 'The Dark Reverser' # The author of this plugin + version = (0, 1, 6) # The version number of this plugin + file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to + on_import = True # Run this plugin during the import + + def run(self, path_to_ebook): + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + PID = self.site_customization + data_file = file(path_to_ebook, 'rb').read() + ar = PID.split(',') + for i in ar: + try: + unlocked_file = DrmStripper(data_file, i).getResult() + except DrmException: + # ignore the error + pass + else: + of = self.temporary_file('.mobi') + of.write(unlocked_file) + of.close() + return of.name + if is_ok_to_use_qt(): + d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook) + d.show() + d.raise_() + d.exec_() + return path_to_ebook + + def customization_help(self, gui=False): + return 'Enter PID (separate multiple PIDs with comma)' + +if __name__ == "__main__": + sys.stdout=Unbuffered(sys.stdout) + print ('MobiDeDrm v%(__version__)s. ' + 'Copyright 2008-2010 The Dark Reverser.' % globals()) + if len(sys.argv)<4: + print "Removes protection from Mobipocket books" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + infile = sys.argv[1] + outfile = sys.argv[2] + pid = sys.argv[3] + data_file = file(infile, 'rb').read() + try: + strippedFile = DrmStripper(data_file, pid) + file(outfile, 'wb').write(strippedFile.getResult()) + except DrmException, e: + print "Error: %s" % e + sys.exit(1) + sys.exit(0) \ No newline at end of file diff --git a/Kindle_4_Mac_Tools/lib/scrolltextwidget.py b/Kindle_4_Mac_Tools/lib/scrolltextwidget.py new file mode 100644 index 0000000..98b4147 --- /dev/null +++ b/Kindle_4_Mac_Tools/lib/scrolltextwidget.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import Tkinter +import Tkconstants + +# basic scrolled text widget +class ScrolledText(Tkinter.Text): + def __init__(self, master=None, **kw): + self.frame = Tkinter.Frame(master) + self.vbar = Tkinter.Scrollbar(self.frame) + self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) + kw.update({'yscrollcommand': self.vbar.set}) + Tkinter.Text.__init__(self, self.frame, **kw) + self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) + self.vbar['command'] = self.yview + # Copy geometry methods of self.frame without overriding Text + # methods = hack! + text_meths = vars(Tkinter.Text).keys() + methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() + methods = set(methods).difference(text_meths) + for m in methods: + if m[0] != '_' and m != 'config' and m != 'configure': + setattr(self, m, getattr(self.frame, m)) + + def __str__(self): + return str(self.frame) diff --git a/Kindle_4_Mac_Tools/lib/subasyncio.py b/Kindle_4_Mac_Tools/lib/subasyncio.py new file mode 100644 index 0000000..ed13aa1 --- /dev/null +++ b/Kindle_4_Mac_Tools/lib/subasyncio.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import os, sys +import signal +import threading +import subprocess +from subprocess import Popen, PIPE, STDOUT + +# **heavily** chopped up and modfied version of asyncproc.py +# to make it actually work on Windows as well as Mac/Linux +# For the original see: +# "http://www.lysator.liu.se/~bellman/download/" +# author is "Thomas Bellman " +# available under GPL version 3 or Later + +# create an asynchronous subprocess whose output can be collected in +# a non-blocking manner + +# What a mess! Have to use threads just to get non-blocking io +# in a cross-platform manner + +# luckily all thread use is hidden within this class + +class Process(object): + def __init__(self, *params, **kwparams): + if len(params) <= 3: + kwparams.setdefault('stdin', subprocess.PIPE) + if len(params) <= 4: + kwparams.setdefault('stdout', subprocess.PIPE) + if len(params) <= 5: + kwparams.setdefault('stderr', subprocess.PIPE) + self.__pending_input = [] + self.__collected_outdata = [] + self.__collected_errdata = [] + self.__exitstatus = None + self.__lock = threading.Lock() + self.__inputsem = threading.Semaphore(0) + self.__quit = False + + self.__process = subprocess.Popen(*params, **kwparams) + + if self.__process.stdin: + self.__stdin_thread = threading.Thread( + name="stdin-thread", + target=self.__feeder, args=(self.__pending_input, + self.__process.stdin)) + self.__stdin_thread.setDaemon(True) + self.__stdin_thread.start() + + if self.__process.stdout: + self.__stdout_thread = threading.Thread( + name="stdout-thread", + target=self.__reader, args=(self.__collected_outdata, + self.__process.stdout)) + self.__stdout_thread.setDaemon(True) + self.__stdout_thread.start() + + if self.__process.stderr: + self.__stderr_thread = threading.Thread( + name="stderr-thread", + target=self.__reader, args=(self.__collected_errdata, + self.__process.stderr)) + self.__stderr_thread.setDaemon(True) + self.__stderr_thread.start() + + def pid(self): + return self.__process.pid + + def kill(self, signal): + self.__process.send_signal(signal) + + # check on subprocess (pass in 'nowait') to act like poll + def wait(self, flag): + if flag.lower() == 'nowait': + rc = self.__process.poll() + else: + rc = self.__process.wait() + if rc != None: + if self.__process.stdin: + self.closeinput() + if self.__process.stdout: + self.__stdout_thread.join() + if self.__process.stderr: + self.__stderr_thread.join() + return self.__process.returncode + + def terminate(self): + if self.__process.stdin: + self.closeinput() + self.__process.terminate() + + # thread gets data from subprocess stdout + def __reader(self, collector, source): + while True: + data = os.read(source.fileno(), 65536) + self.__lock.acquire() + collector.append(data) + self.__lock.release() + if data == "": + source.close() + break + return + + # thread feeds data to subprocess stdin + def __feeder(self, pending, drain): + while True: + self.__inputsem.acquire() + self.__lock.acquire() + if not pending and self.__quit: + drain.close() + self.__lock.release() + break + data = pending.pop(0) + self.__lock.release() + drain.write(data) + + # non-blocking read of data from subprocess stdout + def read(self): + self.__lock.acquire() + outdata = "".join(self.__collected_outdata) + del self.__collected_outdata[:] + self.__lock.release() + return outdata + + # non-blocking read of data from subprocess stderr + def readerr(self): + self.__lock.acquire() + errdata = "".join(self.__collected_errdata) + del self.__collected_errdata[:] + self.__lock.release() + return errdata + + # non-blocking write to stdin of subprocess + def write(self, data): + if self.__process.stdin is None: + raise ValueError("Writing to process with stdin not a pipe") + self.__lock.acquire() + self.__pending_input.append(data) + self.__inputsem.release() + self.__lock.release() + + # close stdinput of subprocess + def closeinput(self): + self.__lock.acquire() + self.__quit = True + self.__inputsem.release() + self.__lock.release() + diff --git a/Topaz_Tools/TopazExtract_Kindle_iPhone.pyw b/Kindle_4_PC_Tools/K4PCDeDRM.pyw similarity index 62% rename from Topaz_Tools/TopazExtract_Kindle_iPhone.pyw rename to Kindle_4_PC_Tools/K4PCDeDRM.pyw index 38b9b29..87009c5 100644 --- a/Topaz_Tools/TopazExtract_Kindle_iPhone.pyw +++ b/Kindle_4_PC_Tools/K4PCDeDRM.pyw @@ -3,16 +3,15 @@ import sys sys.path.append('lib') - import os, os.path, urllib import subprocess from subprocess import Popen, PIPE, STDOUT +import subasyncio +from subasyncio import Process import Tkinter import Tkconstants import tkFileDialog import tkMessageBox -import subasyncio -from subasyncio import Process from scrolltextwidget import ScrolledText class MainDialog(Tkinter.Frame): @@ -21,39 +20,32 @@ class MainDialog(Tkinter.Frame): self.root = root self.interval = 2000 self.p2 = None - self.status = Tkinter.Label(self, text='Extract Contents of Topaz eBook to a Directory') + self.status = Tkinter.Label(self, text='Remove Encryption from a K4PC eBook') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text='Topaz eBook input file').grid(row=0, sticky=Tkconstants.E) - self.tpzpath = Tkinter.Entry(body, width=50) - self.tpzpath.grid(row=0, column=1, sticky=sticky) + Tkinter.Label(body, text='K4PC eBook input file').grid(row=0, sticky=Tkconstants.E) + self.mobipath = Tkinter.Entry(body, width=50) + self.mobipath.grid(row=0, column=1, sticky=sticky) cwd = os.getcwdu() cwd = cwd.encode('utf-8') - self.tpzpath.insert(0, cwd) - button = Tkinter.Button(body, text="...", command=self.get_tpzpath) + self.mobipath.insert(0, cwd) + button = Tkinter.Button(body, text="...", command=self.get_mobipath) button.grid(row=0, column=2) - Tkinter.Label(body, text='Output Directory').grid(row=1, sticky=Tkconstants.E) + Tkinter.Label(body, text='Name for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E) self.outpath = Tkinter.Entry(body, width=50) self.outpath.grid(row=1, column=1, sticky=sticky) - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - self.outpath.insert(0, cwd) + self.outpath.insert(0, '') button = Tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=1, column=2) - Tkinter.Label(body, text='First 8 characters of PID').grid(row=3, sticky=Tkconstants.E) - self.pidnum = Tkinter.StringVar() - self.ccinfo = Tkinter.Entry(body, width=10, textvariable=self.pidnum) - self.ccinfo.grid(row=3, column=1, sticky=sticky) - msg1 = 'Conversion Log \n\n' self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD) - self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky) + self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky) self.stext.insert(Tkconstants.END,msg1) buttons = Tkinter.Frame(self) @@ -75,9 +67,9 @@ class MainDialog(Tkinter.Frame): if poll != None: text = self.p2.readerr() text += self.p2.read() - msg = text + '\n\n' + 'Files successfully extracted\n' + msg = text + '\n\n' + 'Encryption successfully removed\n' if poll != 0: - msg = text + '\n\n' + 'Error: File Extraction Failed\n' + msg = text + '\n\n' + 'Error: Encryption Removal Failed\n' self.showCmdOutput(msg) self.p2 = None self.sbotton.configure(state='normal') @@ -98,41 +90,43 @@ class MainDialog(Tkinter.Frame): return # run as a subprocess via pipes and collect stdout - def topazrdr(self, infile, outdir, pidnum): + def mobirdr(self, infile, outfile): # os.putenv('PYTHONUNBUFFERED', '1') - pidoption = ' -p "' + pidnum + '" ' - outoption = ' -o "' + outdir + '" ' - cmdline = 'python ./lib/cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"' + cmdline = 'python ./lib/k4pcdedrm.py "' + infile + '" "' + outfile + '"' if sys.platform[0:3] == 'win': search_path = os.environ['PATH'] search_path = search_path.lower() if search_path.find('python') >= 0: - cmdline = 'python lib\cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"' + cmdline = 'python lib\k4pcdedrm.py "' + infile + '" "' + outfile + '"' else : - cmdline = 'lib\cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"' + cmdline = 'lib\k4pcdedrm.py "' + infile + '" "' + outfile + '"' cmdline = cmdline.encode(sys.getfilesystemencoding()) p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False) return p2 - def get_tpzpath(self): - tpzpath = tkFileDialog.askopenfilename( - parent=None, title='Select Topaz File', - defaultextension='.prc', filetypes=[('Topaz azw1', '.azw1'), ('Topaz prc', '.prc'), + def get_mobipath(self): + mobipath = tkFileDialog.askopenfilename( + parent=None, title='Select K4PC eBook File', + defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'), ('All Files', '.*')]) - if tpzpath: - tpzpath = os.path.normpath(tpzpath) - self.tpzpath.delete(0, Tkconstants.END) - self.tpzpath.insert(0, tpzpath) + if mobipath: + mobipath = os.path.normpath(mobipath) + self.mobipath.delete(0, Tkconstants.END) + self.mobipath.insert(0, mobipath) return def get_outpath(self): - cwd = os.getcwdu() - cwd = cwd.encode('utf-8') - outpath = tkFileDialog.askdirectory( - parent=None, title='Directory to Extract Files into', - initialdir=cwd, initialfile=None) + mobipath = self.mobipath.get() + initname = os.path.basename(mobipath) + p = initname.find('.') + if p >= 0: initname = initname[0:p] + initname += '_nodrm.mobi' + outpath = tkFileDialog.asksaveasfilename( + parent=None, title='Select Unencrypted Mobi File to produce', + defaultextension='.mobi', initialfile=initname, + filetypes=[('Mobi files', '.mobi'), ('All files', '.*')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, Tkconstants.END) @@ -150,33 +144,25 @@ class MainDialog(Tkinter.Frame): def convertit(self): # now disable the button to prevent multiple launches self.sbotton.configure(state='disabled') - tpzpath = self.tpzpath.get() + mobipath = self.mobipath.get() outpath = self.outpath.get() - if not tpzpath or not os.path.exists(tpzpath): - self.status['text'] = 'Specified Topaz eBook file does not exist' + if not mobipath or not os.path.exists(mobipath): + self.status['text'] = 'Specified K4PC eBook file does not exist' self.sbotton.configure(state='normal') return if not outpath: - self.status['text'] = 'No output directory specified' - self.sbotton.configure(state='normal') - return - if not os.path.exists(outpath): - os.makedirs(outpath) - pidnum = self.pidnum.get() - if not pidnum or pidnum == '': - self.status['text'] = 'You have not entered a PID ' + self.status['text'] = 'No output file specified' self.sbotton.configure(state='normal') return - log = 'Command = "python cmbtc_dump_nonK4PC.py"\n' - log += 'Topaz Path Path = "'+ tpzpath + '"\n' - log += 'Output Directory = "' + outpath + '"\n' - log += 'First 8 chars of PID = "' + pidnum + '"\n' + log = 'Command = "python k4pcdedrm.py"\n' + log += 'K4PC Path = "'+ mobipath + '"\n' + log += 'Output File = "' + outpath + '"\n' log += '\n\n' - log += 'Please Wait ...\n' + log += 'Please Wait ...\n\n' log = log.encode('utf-8') self.stext.insert(Tkconstants.END,log) - self.p2 = self.topazrdr(tpzpath, outpath, pidnum) + self.p2 = self.mobirdr(mobipath, outpath) # python does not seem to allow you to create # your own eventloop which every other gui does - strange @@ -188,7 +174,7 @@ class MainDialog(Tkinter.Frame): def main(argv=None): root = Tkinter.Tk() - root.title('Topaz eBook File Extraction') + root.title('K4PC eBook Encryption Removal') root.resizable(True, False) root.minsize(300, 0) MainDialog(root).pack(fill=Tkconstants.X, expand=1) diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.cpp b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.cpp similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.cpp rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.cpp diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.exe b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.exe similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.exe rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/EZskindle4PCv1_1_1.exe diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle Read Me.txt b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle Read Me.txt similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle Read Me.txt rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle Read Me.txt diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.cpp b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.cpp similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.cpp rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.cpp diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.exe b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.exe similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.exe rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/LZskindle4PCv1_1.exe diff --git a/Kindle_Mobi_Tools/LZskindle4PCv1_1/ReadMe.txt b/Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/ReadMe.txt similarity index 100% rename from Kindle_Mobi_Tools/LZskindle4PCv1_1/ReadMe.txt rename to Kindle_4_PC_Tools/Other_Tools/LZskindle4PCv1_1/ReadMe.txt diff --git a/Kindle_4_PC_Tools/Other_Tools/README_Other_K4PC_Tools.txt b/Kindle_4_PC_Tools/Other_Tools/README_Other_K4PC_Tools.txt new file mode 100644 index 0000000..ff1513c --- /dev/null +++ b/Kindle_4_PC_Tools/Other_Tools/README_Other_K4PC_Tools.txt @@ -0,0 +1,9 @@ +Readme.txt + +- LZskindle4PC is a front end for running skindle that makes it easier for some people who do not like command lines to use it. + +- skindle has completely reverse engineered how the book specific PID is generated for Kindle4PC so it can be very useful when new version of Kindle4PC are released and unswindle has not yet been updated. Unfortunately, skindle has some minor bugs that can actually result in corrupted eBooks. No one has yet tracked them down and fixed them. Until they do, use at your own risk. + +- unswindle can be used to find the book specific PID but it needs to be updated for each version of Kindle4PC that Amazon releases (and therefore is also useful for Linux users who have Wine). This program “patches” the Kindle4PC executable and therefore is very release specific. +Unfortunately unswindle v7 the latest, has not been updated to work with the latest version of Kindle for PC. You will need to find one of the older versions of Kindle4PC and prevent later updates in order to use this tool. + diff --git a/Kindle_Mobi_Tools/skindle-06/Makefile b/Kindle_4_PC_Tools/Other_Tools/skindle-06/Makefile similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/Makefile rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/Makefile diff --git a/Kindle_Mobi_Tools/skindle-06/README.txt b/Kindle_4_PC_Tools/Other_Tools/skindle-06/README similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/README.txt rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/README diff --git a/Kindle_Mobi_Tools/skindle-06/b64.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/b64.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/b64.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/b64.c diff --git a/Kindle_Mobi_Tools/skindle-06/cbuf.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/cbuf.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/cbuf.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/cbuf.c diff --git a/Kindle_Mobi_Tools/skindle-06/cbuf.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/cbuf.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/cbuf.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/cbuf.h diff --git a/Kindle_Mobi_Tools/skindle-06/libz.a b/Kindle_4_PC_Tools/Other_Tools/skindle-06/libz.a similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/libz.a rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/libz.a diff --git a/Kindle_Mobi_Tools/skindle-06/md5.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/md5.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/md5.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/md5.c diff --git a/Kindle_Mobi_Tools/skindle-06/md5.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/md5.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/md5.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/md5.h diff --git a/Kindle_Mobi_Tools/skindle-06/mobi.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/mobi.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/mobi.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/mobi.c diff --git a/Kindle_Mobi_Tools/skindle-06/mobi.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/mobi.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/mobi.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/mobi.h diff --git a/Kindle_Mobi_Tools/skindle-06/sha1.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/sha1.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/sha1.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/sha1.c diff --git a/Kindle_Mobi_Tools/skindle-06/sha1.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/sha1.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/sha1.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/sha1.h diff --git a/Kindle_Mobi_Tools/skindle-06/skindle.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/skindle.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/skindle.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/skindle.c diff --git a/Kindle_Mobi_Tools/skindle-06/skindle.exe b/Kindle_4_PC_Tools/Other_Tools/skindle-06/skindle.exe similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/skindle.exe rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/skindle.exe diff --git a/Kindle_Mobi_Tools/skindle-06/skinutils.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/skinutils.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/skinutils.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/skinutils.c diff --git a/Kindle_Mobi_Tools/skindle-06/skinutils.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/skinutils.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/skinutils.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/skinutils.h diff --git a/Kindle_Mobi_Tools/skindle-06/tpz.c b/Kindle_4_PC_Tools/Other_Tools/skindle-06/tpz.c similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/tpz.c rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/tpz.c diff --git a/Kindle_Mobi_Tools/skindle-06/tpz.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/tpz.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/tpz.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/tpz.h diff --git a/Kindle_Mobi_Tools/skindle-06/zconf.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/zconf.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/zconf.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/zconf.h diff --git a/Kindle_Mobi_Tools/skindle-06/zlib.h b/Kindle_4_PC_Tools/Other_Tools/skindle-06/zlib.h similarity index 100% rename from Kindle_Mobi_Tools/skindle-06/zlib.h rename to Kindle_4_PC_Tools/Other_Tools/skindle-06/zlib.h diff --git a/Kindle_4_PC_Tools/Other_Tools/unswindle/mobidedrm.py b/Kindle_4_PC_Tools/Other_Tools/unswindle/mobidedrm.py new file mode 100644 index 0000000..07d5f6f --- /dev/null +++ b/Kindle_4_PC_Tools/Other_Tools/unswindle/mobidedrm.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# It can run standalone to convert files, or it can be installed as a +# plugin for Calibre (http://calibre-ebook.com/about) so that +# importing files with DRM 'Just Works'. +# +# To create a Calibre plugin, rename this file so that the filename +# ends in '_plugin.py', put it into a ZIP file and import that Calibre +# using its plugin configuration GUI. +# +# Changelog +# 0.01 - Initial version +# 0.02 - Huffdic compressed books were not properly decrypted +# 0.03 - Wasn't checking MOBI header length +# 0.04 - Wasn't sanity checking size of data record +# 0.05 - It seems that the extra data flags take two bytes not four +# 0.06 - And that low bit does mean something after all :-) +# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size +# 0.08 - ...and also not in Mobi header version < 6 +# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! +# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre +# import filter it works when importing unencrypted files. +# Also now handles encrypted files that don't need a specific PID. +# 0.11 - use autoflushed stdout and proper return values +# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors +# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace +# and extra blank lines, converted CR/LF pairs at ends of each line, +# and other cosmetic fixes. +# 0.14 - Working out when the extra data flags are present has been problematic +# Versions 7 through 9 have tried to tweak the conditions, but have been +# only partially successful. Closer examination of lots of sample +# files reveals that a confusin has arisen because trailing data entries +# are not encrypted, but it turns out that the multibyte entries +# in utf8 file are encrypted. (Although neither kind gets compressed.) +# This knowledge leads to a simplification of the test for the +# trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'hearbeat', and is also quicker for long files. +# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. + +__version__ = '0.16' + +import sys +import struct +import binascii + +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +class DrmException(Exception): + pass + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + +def checksumPid(s): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + crc = (~binascii.crc32(s,-1))&0xFFFFFFFF + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + return res + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Multibyte data, if present, is included in the encryption, so + # we do not need to check the low bit. + # if flags & 1: + # num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + +class DrmStripper: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pid): + pid = pid.ljust(16,'\0') + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + temp_key = PC1(keyvec1, pid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: + found_key = finalkey + break + if not found_key: + # Then try the default encoding that doesn't require a PID + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum: + found_key = finalkey + break + return found_key + + def __init__(self, data_file, pid): + if checksumPid(pid[0:-2]) != pid: + raise DrmException("invalid PID checksum") + pid = pid[0:-2] + + self.data_file = data_file + header = data_file[0:72] + if header[0x3C:0x3C+8] != 'BOOKMOBI': + raise DrmException("invalid file format") + self.num_sections, = struct.unpack('>H', data_file[76:78]) + + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + + sect = self.loadSection(0) + records, = struct.unpack('>H', sect[0x8:0x8+2]) + mobi_length, = struct.unpack('>L',sect[0x14:0x18]) + mobi_version, = struct.unpack('>L',sect[0x68:0x6C]) + extra_data_flags = 0 + print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length) + if (mobi_length >= 0xE4) and (mobi_version >= 5): + extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4]) + print "Extra Data Flags = %d" %extra_data_flags + + crypto_type, = struct.unpack('>H', sect[0xC:0xC+2]) + if crypto_type == 0: + print "This book is not encrypted." + else: + if crypto_type == 1: + raise DrmException("cannot decode Mobipocket encryption type 1") + if crypto_type != 2: + raise DrmException("unknown encryption type: %d" % crypto_type) + + # calculate the keys + drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("no PIDs found in this file") + found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) + if not found_key: + raise DrmException("no key found. maybe the PID is incorrect") + + # kill the drm keys + self.patchSection(0, "\0" * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print "Decrypting. Please wait . . .", + new_data = self.data_file[:self.sections[1][0]] + for i in xrange(1, records+1): + data = self.loadSection(i) + extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags) + if i%100 == 0: + print ".", + # print "record %d, extra_size %d" %(i,extra_size) + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done" + + def getResult(self): + return self.data_file + +if not __name__ == "__main__": + from calibre.customize import FileTypePlugin + + class MobiDeDRM(FileTypePlugin): + name = 'MobiDeDRM' # Name of the plugin + description = 'Removes DRM from secure Mobi files' + supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on + author = 'The Dark Reverser' # The author of this plugin + version = (0, 1, 6) # The version number of this plugin + file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to + on_import = True # Run this plugin during the import + + def run(self, path_to_ebook): + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + PID = self.site_customization + data_file = file(path_to_ebook, 'rb').read() + ar = PID.split(',') + for i in ar: + try: + unlocked_file = DrmStripper(data_file, i).getResult() + except DrmException: + # ignore the error + pass + else: + of = self.temporary_file('.mobi') + of.write(unlocked_file) + of.close() + return of.name + if is_ok_to_use_qt(): + d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook) + d.show() + d.raise_() + d.exec_() + return path_to_ebook + + def customization_help(self, gui=False): + return 'Enter PID (separate multiple PIDs with comma)' + +if __name__ == "__main__": + sys.stdout=Unbuffered(sys.stdout) + print ('MobiDeDrm v%(__version__)s. ' + 'Copyright 2008-2010 The Dark Reverser.' % globals()) + if len(sys.argv)<4: + print "Removes protection from Mobipocket books" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + infile = sys.argv[1] + outfile = sys.argv[2] + pid = sys.argv[3] + data_file = file(infile, 'rb').read() + try: + strippedFile = DrmStripper(data_file, pid) + file(outfile, 'wb').write(strippedFile.getResult()) + except DrmException, e: + print "Error: %s" % e + sys.exit(1) + sys.exit(0) \ No newline at end of file diff --git a/Kindle_Mobi_Tools/unswindle/unswindle.pyw b/Kindle_4_PC_Tools/Other_Tools/unswindle/unswindle_v7.pyw similarity index 100% rename from Kindle_Mobi_Tools/unswindle/unswindle.pyw rename to Kindle_4_PC_Tools/Other_Tools/unswindle/unswindle_v7.pyw diff --git a/Kindle_4_PC_Tools/README_K4PCDeDRM.txt b/Kindle_4_PC_Tools/README_K4PCDeDRM.txt new file mode 100644 index 0000000..d9f4780 --- /dev/null +++ b/Kindle_4_PC_Tools/README_K4PCDeDRM.txt @@ -0,0 +1,23 @@ +Readme.txt + +From Apprentice Alf's Blog: + +When Amazon released Kindle for PC, there was a lot of interest in finding the PID used by an installed copy. Unfortunately, it turned out that Amazon had taken the opportunity to increase the complexity of the DRM applied to ebooks downloaded for use with Kindle for PC. In short, every book has its own PID. + +The current best method is a script called K4PCDeDRM.py, which takes code from several previous tools and puts them together, giving a one-script solution to removing the DRM that is not dependent on the exact version of the Kindle for PC software. + +To use this script requires Python Version 2.X 32 bit version. On Windows, we recommend downloading ActiveState's ActivePython for Windows (Version 2.X NOT 3.X (32bit). + +We strongly recommend that at the moment people with Kindle for Windows should use K4PCDeDRM.pyw instead of any of the other methods (skindle, unswindle). + +K4PCDeDRM is available in the large archive mentioned in the first post. + +To run it simply completely extract the tools archive and open the Kindle_4_PC_Tools. Then double-click on K4PCDeDRM.pyw to run the gui version. + +Please note, the tools archive must be on the same machine in the same account as the Kindle for PC application for things to work. + +FYI: +Books downloaded to Kindle for PC are stored in a folder called “My Kindle Content” in the current user’s home folder. + +IMPORTANT: There are two kinds of Kindle ebooks. Mobipocket and Topaz. For Topaz books it’s really necessary to use the Topaz specific tools mentioned in this post which not only remove the DRM but also convert the Topaz file into a format that can be converted into other formats. + diff --git a/Kindle_4_PC_Tools/lib/k4pcdedrm.py b/Kindle_4_PC_Tools/lib/k4pcdedrm.py new file mode 100644 index 0000000..509987e --- /dev/null +++ b/Kindle_4_PC_Tools/lib/k4pcdedrm.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python +# +# This is a WINDOWS python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# It can run standalone to convert K4PC files, or it can be installed as a +# plugin for Calibre (http://calibre-ebook.com/about) so that importing +# K4PC files with DRM is no londer a multi-step process. +# +# ***NOTE*** Calibre and K4PC must be installed on the same windows machine +# for the plugin version to function properly. +# +# To create a Calibre plugin, rename this file so that the filename +# ends in '_plugin.py', put it into a ZIP file and import that ZIP into Calibre +# using its plugin configuration GUI. +# +# Thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump from +# which this script steals most unashamedly. +# +# Changelog +# 0.01 - Initial version - Utilizes skindle and CMBDTC method of obtaining +# book specific pids from K4PC books. If Calibre and K4PC are installed +# on the same windows machine, Calibre plugin functionality is once +# again restored. + + +""" + +Comprehensive Mazama Book DRM with Topaz Cryptography V2.0 + +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6 +M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC +B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx +y2/pHuYme7U1TsgSjwIDAQAB +-----END PUBLIC KEY----- + +""" + +from __future__ import with_statement + +import csv +import sys +import os +import getopt +import zlib +import binascii +from struct import pack +from struct import unpack +from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ + create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ + string_at, Structure, c_void_p, cast +import _winreg as winreg +import traceback +import hashlib + +__version__ = '0.01' + +global kindleDatabase +MAX_PATH = 255 +kernel32 = windll.kernel32 +advapi32 = windll.advapi32 +crypt32 = windll.crypt32 + + +# +# Various character maps used to decrypt books. Probably supposed to act as obfuscation +# +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" +charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + + +# +# Exceptions for all the problems that might happen during the script +# +class DrmException(Exception): + pass + + +class DataBlob(Structure): + _fields_ = [('cbData', c_uint), + ('pbData', c_void_p)] +DataBlob_p = POINTER(DataBlob) + + +def GetSystemDirectory(): + GetSystemDirectoryW = kernel32.GetSystemDirectoryW + GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] + GetSystemDirectoryW.restype = c_uint + def GetSystemDirectory(): + buffer = create_unicode_buffer(MAX_PATH + 1) + GetSystemDirectoryW(buffer, len(buffer)) + return buffer.value + return GetSystemDirectory +GetSystemDirectory = GetSystemDirectory() + + +def GetVolumeSerialNumber(): + GetVolumeInformationW = kernel32.GetVolumeInformationW + GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, + POINTER(c_uint), POINTER(c_uint), + POINTER(c_uint), c_wchar_p, c_uint] + GetVolumeInformationW.restype = c_uint + def GetVolumeSerialNumber(path): + vsn = c_uint(0) + GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) + return vsn.value + return GetVolumeSerialNumber +GetVolumeSerialNumber = GetVolumeSerialNumber() + + +def GetUserName(): + GetUserNameW = advapi32.GetUserNameW + GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] + GetUserNameW.restype = c_uint + def GetUserName(): + buffer = create_unicode_buffer(32) + size = c_uint(len(buffer)) + while not GetUserNameW(buffer, byref(size)): + buffer = create_unicode_buffer(len(buffer) * 2) + size.value = len(buffer) + return buffer.value.encode('utf-16-le')[::2] + return GetUserName +GetUserName = GetUserName() + + +def CryptUnprotectData(): + _CryptUnprotectData = crypt32.CryptUnprotectData + _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, + c_void_p, c_void_p, c_uint, DataBlob_p] + _CryptUnprotectData.restype = c_uint + def CryptUnprotectData(indata, entropy): + indatab = create_string_buffer(indata) + indata = DataBlob(len(indata), cast(indatab, c_void_p)) + entropyb = create_string_buffer(entropy) + entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) + outdata = DataBlob() + if not _CryptUnprotectData(byref(indata), None, byref(entropy), + None, None, 0, byref(outdata)): + raise DrmException("Failed to Unprotect Data") + return string_at(outdata.pbData, outdata.cbData) + return CryptUnprotectData +CryptUnprotectData = CryptUnprotectData() + + +# +# Returns the MD5 digest of "message" +# +def MD5(message): + ctx = hashlib.md5() + ctx.update(message) + return ctx.digest() + + +# +# Returns the MD5 digest of "message" +# +def SHA1(message): + ctx = hashlib.sha1() + ctx.update(message) + return ctx.digest() + + +# +# Locate and open the Kindle.info file. +# +def openKindleInfo(): + regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") + path = winreg.QueryValueEx(regkey, 'Local AppData')[0] + return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r') + + +# +# Parse the Kindle.info file and return the records as a list of key-values +# +def parseKindleInfo(): + DB = {} + infoReader = openKindleInfo() + infoReader.read(1) + data = infoReader.read() + items = data.split('{') + + for item in items: + splito = item.split(':') + DB[splito[0]] =splito[1] + return DB + + +# +# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal) +# +def findNameForHash(hash): + names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"] + result = "" + for name in names: + if hash == encodeHash(name, charMap2): + result = name + break + return name + + +# +# Print all the records from the kindle.info file. +# +def printKindleInfo(): + for record in kindleDatabase: + name = findNameForHash(record) + if name != "" : + print (name) + print ("--------------------------\n") + else : + print ("Unknown Record") + print getKindleInfoValueForHash(record) + print "\n" + + +# +# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record +# +def getKindleInfoValueForHash(hashedKey): + global kindleDatabase + encryptedValue = decode(kindleDatabase[hashedKey],charMap2) + return CryptUnprotectData(encryptedValue,"") + + +# +# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record +# +def getKindleInfoValueForKey(key): + return getKindleInfoValueForHash(encodeHash(key,charMap2)) + + +# +# 8 bits to six bits encoding from hash to generate PID string +# +def encodePID(hash): + global charMap3 + PID = "" + for position in range (0,8): + PID += charMap3[getSixBitsFromBitField(hash,position)] + return PID + + +# +# Hash the bytes in data and then encode the digest with the characters in map +# +def encodeHash(data,map): + return encode(MD5(data),map) + + +# +# Encode the bytes in data with the characters in map +# +def encode(data, map): + result = "" + for char in data: + value = ord(char) + Q = (value ^ 0x80) // len(map) + R = value % len(map) + result += map[Q] + result += map[R] + return result + + +# +# Decode the string in data with the characters in map. Returns the decoded bytes +# +def decode(data,map): + result = "" + for i in range (0,len(data),2): + high = map.find(data[i]) + low = map.find(data[i+1]) + value = (((high * 0x40) ^ 0x80) & 0xFF) + low + result += pack("B",value) + return result + + +# +# Encryption table used to generate the device PID +# +def generatePidEncryptionTable() : + table = [] + for counter1 in range (0,0x100): + value = counter1 + for counter2 in range (0,8): + if (value & 1 == 0) : + value = value >> 1 + else : + value = value >> 1 + value = value ^ 0xEDB88320 + table.append(value) + return table + + +# +# Seed value used to generate the device PID +# +def generatePidSeed(table,dsn) : + value = 0 + for counter in range (0,4) : + index = (ord(dsn[counter]) ^ value) &0xFF + value = (value >> 8) ^ table[index] + return value + + +# +# Generate the device PID +# +def generateDevicePID(table,dsn,nbRoll): + seed = generatePidSeed(table,dsn) + pidAscii = "" + pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] + index = 0 + + for counter in range (0,nbRoll): + pid[index] = pid[index] ^ ord(dsn[counter]) + index = (index+1) %8 + + for counter in range (0,8): + index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) + pidAscii += charMap4[index] + return pidAscii + + +# +# Returns two bit at offset from a bit field +# +def getTwoBitsFromBitField(bitField,offset): + byteNumber = offset // 4 + bitPosition = 6 - 2*(offset % 4) + + return ord(bitField[byteNumber]) >> bitPosition & 3 + + +# +# Returns the six bits at offset from a bit field +# +def getSixBitsFromBitField(bitField,offset): + offset *= 3 + value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) + return value + + +# +# MobiDeDrm-0.16 Stuff +# +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Multibyte data, if present, is included in the encryption, so + # we do not need to check the low bit. + # if flags & 1: + # num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + + +# +# This class does all the heavy lifting. +# +class DrmStripper: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pid): + pid = pid.ljust(16,'\0') + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + temp_key = PC1(keyvec1, pid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1: + found_key = finalkey + break + if not found_key: + # Then try the default encoding that doesn't require a PID + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie) + if verification == ver and cksum == temp_key_sum: + found_key = finalkey + break + return found_key + + def __init__(self, data_file): + self.data_file = data_file + header = data_file[0:72] + if header[0x3C:0x3C+8] != 'BOOKMOBI': + raise DrmException("invalid file format") + self.num_sections, = unpack('>H', data_file[76:78]) + + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = unpack('>LBBBB', data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + + sect = self.loadSection(0) + records, = unpack('>H', sect[0x8:0x8+2]) + mobi_length, = unpack('>L',sect[0x14:0x18]) + mobi_version, = unpack('>L',sect[0x68:0x6C]) + extra_data_flags = 0 + print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length) + if (mobi_length >= 0xE4) and (mobi_version >= 5): + extra_data_flags, = unpack('>H', sect[0xF2:0xF4]) + print "Extra Data Flags = %d" %extra_data_flags + + crypto_type, = unpack('>H', sect[0xC:0xC+2]) + if crypto_type == 0: + print "This book is not encrypted." + else: + if crypto_type == 1: + raise DrmException("cannot decode Mobipocket encryption type 1") + if crypto_type != 2: + raise DrmException("unknown encryption type: %d" % crypto_type) + + # determine the EXTH Offset. + exth_off = unpack('>I', sect[20:24])[0] + 16 + self.sections[0][0] + # Grab the entire EXTH block and feed it to the getK4PCPids function. + exth = data_file[exth_off:self.sections[0+1][0]] + pid = getK4PCPids(exth) + + # calculate the keys + drm_ptr, drm_count, drm_size, drm_flags = unpack('>LLLL', sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("no PIDs found in this file") + found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid) + if not found_key: + raise DrmException("no key found. maybe the PID is incorrect") + + # kill the drm keys + self.patchSection(0, "\0" * drm_size, drm_ptr) + # kill the drm pointers + self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8) + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print "\nDecrypting. Please wait . . .", + new_data = self.data_file[:self.sections[1][0]] + for i in xrange(1, records+1): + data = self.loadSection(i) + extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags) + if i%100 == 0: + print ".", + # print "record %d, extra_size %d" %(i,extra_size) + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + #self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size])) + if self.num_sections > records+1: + new_data += self.data_file[self.sections[records+1][0]:] + self.data_file = new_data + print "done!" + print "\nPlease only use your new-found powers for good." + + def getResult(self): + return self.data_file + + +# +# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo +# file to calculate the book pid. +# +def getK4PCPids(exth): + global kindleDatabase + try: + kindleDatabase = parseKindleInfo() + except Exception as message: + print(message) + + if kindleDatabase != None : + + # Get the Mazama Random number + MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber") + + # Get the HDD serial + encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1) + + # Get the current user name + encodedUsername = encodeHash(GetUserName(),charMap1) + + # concat, hash and encode to calculate the DSN + DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + + print("\nDSN: " + DSN) + + + # Compute the device PID (for which I can tell, is used for nothing). + # But hey, stuff being printed out is apparently cool. + table = generatePidEncryptionTable() + devicePID = generateDevicePID(table,DSN,4) + + print("Device PID: " + devicePID) + + # Compute book PID + exth_records = {} + nitems, = unpack('>I', exth[8:12]) + pos = 12 + # Parse the EXTH records, storing data indexed by type + for i in xrange(nitems): + type, size = unpack('>II', exth[pos: pos + 8]) + content = exth[pos + 8: pos + size] + + exth_records[type] = content + pos += size + + # Grab the contents of the type 209 exth record + if exth_records[209] != None: + data = exth_records[209] + else: + raise DrmException("\nNo EXTH record type 209 - Perhaps not a K4PC file?") + + # Parse the 209 data to find the the exth record with the token data. + # The last character of the 209 data points to the record with the token. + # Always 208 from my experience, but I'll leave the logic in case that changes. + for i in xrange(len(data)): + if ord(data[i]) != 0: + if exth_records[ord(data[i])] != None: + token = exth_records[ord(data[i])] + + # Get the kindle account token + kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens") + + print("Account Token: " + kindleAccountToken) + + pidHash = SHA1(DSN+kindleAccountToken+exth_records[209]+token) + + bookPID = encodePID(pidHash) + + if exth_records[503] != None: + print "Pid for " + exth_records[503] + ": " + bookPID + else: + print ("Book PID: " + bookPID ) + + return bookPID + + raise DrmException("\nCould not access K4PC data - Perhaps K4PC is not installed/configured?") + return null + +if not __name__ == "__main__": + from calibre.customize import FileTypePlugin + + class K4PCDeDRM(FileTypePlugin): + name = 'K4PCDeDRM' # Name of the plugin + description = 'Removes DRM from K4PC files' + supported_platforms = ['windows'] # Platforms this plugin will run on + author = 'DiapDealer' # The author of this plugin + version = (0, 0, 1) # The version number of this plugin + file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to + on_import = True # Run this plugin during the import + + def run(self, path_to_ebook): + from calibre.gui2 import is_ok_to_use_qt + from PyQt4.Qt import QMessageBox + data_file = file(path_to_ebook, 'rb').read() + + try: + unlocked_file = DrmStripper(data_file).getResult() + except DrmException: + # ignore the error + pass + else: + of = self.temporary_file('.mobi') + of.write(unlocked_file) + of.close() + return of.name + + if is_ok_to_use_qt(): + d = QMessageBox(QMessageBox.Warning, "K4PCDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook) + d.show() + d.raise_() + d.exec_() + return path_to_ebook + + #def customization_help(self, gui=False): + # return 'Enter PID (separate multiple PIDs with comma)' + +if __name__ == "__main__": + sys.stdout=Unbuffered(sys.stdout) + print ('K4PCDeDrm v%(__version__)s ' + 'provided DiapDealer.' % globals()) + if len(sys.argv)<3: + print "Removes DRM protection from K4PC books" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + infile = sys.argv[1] + outfile = sys.argv[2] + data_file = file(infile, 'rb').read() + try: + strippedFile = DrmStripper(data_file) + file(outfile, 'wb').write(strippedFile.getResult()) + except DrmException, e: + print "Error: %s" % e + sys.exit(1) + sys.exit(0) \ No newline at end of file diff --git a/Kindle_4_PC_Tools/lib/scrolltextwidget.py b/Kindle_4_PC_Tools/lib/scrolltextwidget.py new file mode 100644 index 0000000..98b4147 --- /dev/null +++ b/Kindle_4_PC_Tools/lib/scrolltextwidget.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import Tkinter +import Tkconstants + +# basic scrolled text widget +class ScrolledText(Tkinter.Text): + def __init__(self, master=None, **kw): + self.frame = Tkinter.Frame(master) + self.vbar = Tkinter.Scrollbar(self.frame) + self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) + kw.update({'yscrollcommand': self.vbar.set}) + Tkinter.Text.__init__(self, self.frame, **kw) + self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) + self.vbar['command'] = self.yview + # Copy geometry methods of self.frame without overriding Text + # methods = hack! + text_meths = vars(Tkinter.Text).keys() + methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() + methods = set(methods).difference(text_meths) + for m in methods: + if m[0] != '_' and m != 'config' and m != 'configure': + setattr(self, m, getattr(self.frame, m)) + + def __str__(self): + return str(self.frame) diff --git a/Kindle_4_PC_Tools/lib/subasyncio.py b/Kindle_4_PC_Tools/lib/subasyncio.py new file mode 100644 index 0000000..ed13aa1 --- /dev/null +++ b/Kindle_4_PC_Tools/lib/subasyncio.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import os, sys +import signal +import threading +import subprocess +from subprocess import Popen, PIPE, STDOUT + +# **heavily** chopped up and modfied version of asyncproc.py +# to make it actually work on Windows as well as Mac/Linux +# For the original see: +# "http://www.lysator.liu.se/~bellman/download/" +# author is "Thomas Bellman " +# available under GPL version 3 or Later + +# create an asynchronous subprocess whose output can be collected in +# a non-blocking manner + +# What a mess! Have to use threads just to get non-blocking io +# in a cross-platform manner + +# luckily all thread use is hidden within this class + +class Process(object): + def __init__(self, *params, **kwparams): + if len(params) <= 3: + kwparams.setdefault('stdin', subprocess.PIPE) + if len(params) <= 4: + kwparams.setdefault('stdout', subprocess.PIPE) + if len(params) <= 5: + kwparams.setdefault('stderr', subprocess.PIPE) + self.__pending_input = [] + self.__collected_outdata = [] + self.__collected_errdata = [] + self.__exitstatus = None + self.__lock = threading.Lock() + self.__inputsem = threading.Semaphore(0) + self.__quit = False + + self.__process = subprocess.Popen(*params, **kwparams) + + if self.__process.stdin: + self.__stdin_thread = threading.Thread( + name="stdin-thread", + target=self.__feeder, args=(self.__pending_input, + self.__process.stdin)) + self.__stdin_thread.setDaemon(True) + self.__stdin_thread.start() + + if self.__process.stdout: + self.__stdout_thread = threading.Thread( + name="stdout-thread", + target=self.__reader, args=(self.__collected_outdata, + self.__process.stdout)) + self.__stdout_thread.setDaemon(True) + self.__stdout_thread.start() + + if self.__process.stderr: + self.__stderr_thread = threading.Thread( + name="stderr-thread", + target=self.__reader, args=(self.__collected_errdata, + self.__process.stderr)) + self.__stderr_thread.setDaemon(True) + self.__stderr_thread.start() + + def pid(self): + return self.__process.pid + + def kill(self, signal): + self.__process.send_signal(signal) + + # check on subprocess (pass in 'nowait') to act like poll + def wait(self, flag): + if flag.lower() == 'nowait': + rc = self.__process.poll() + else: + rc = self.__process.wait() + if rc != None: + if self.__process.stdin: + self.closeinput() + if self.__process.stdout: + self.__stdout_thread.join() + if self.__process.stderr: + self.__stderr_thread.join() + return self.__process.returncode + + def terminate(self): + if self.__process.stdin: + self.closeinput() + self.__process.terminate() + + # thread gets data from subprocess stdout + def __reader(self, collector, source): + while True: + data = os.read(source.fileno(), 65536) + self.__lock.acquire() + collector.append(data) + self.__lock.release() + if data == "": + source.close() + break + return + + # thread feeds data to subprocess stdin + def __feeder(self, pending, drain): + while True: + self.__inputsem.acquire() + self.__lock.acquire() + if not pending and self.__quit: + drain.close() + self.__lock.release() + break + data = pending.pop(0) + self.__lock.release() + drain.write(data) + + # non-blocking read of data from subprocess stdout + def read(self): + self.__lock.acquire() + outdata = "".join(self.__collected_outdata) + del self.__collected_outdata[:] + self.__lock.release() + return outdata + + # non-blocking read of data from subprocess stderr + def readerr(self): + self.__lock.acquire() + errdata = "".join(self.__collected_errdata) + del self.__collected_errdata[:] + self.__lock.release() + return errdata + + # non-blocking write to stdin of subprocess + def write(self, data): + if self.__process.stdin is None: + raise ValueError("Writing to process with stdin not a pipe") + self.__lock.acquire() + self.__pending_input.append(data) + self.__inputsem.release() + self.__lock.release() + + # close stdinput of subprocess + def closeinput(self): + self.__lock.acquire() + self.__quit = True + self.__inputsem.release() + self.__lock.release() + diff --git a/Kindle_Mobi_Tools/PIDCheck.py b/Kindle_Mobi_Tools/PIDCheck.py new file mode 100644 index 0000000..1960453 --- /dev/null +++ b/Kindle_Mobi_Tools/PIDCheck.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# Changelog +# 1.00 - Initial version + +__version__ = '1.00' + +import sys +import struct +import binascii + +def checksumPid(s): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + crc = (~binascii.crc32(s,-1))&0xFFFFFFFF + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + return res + +if __name__ == "__main__": + if len(sys.argv) != 2: + print "Checks Mobipocket PID checksum" + print "Usage:" + print " %s " % sys.argv[0] + sys.exit(1) + else: + pid = sys.argv[1] + if len(pid) == 8: + pid = checksumPid(pid) + else: + pid = checksumPid(pid[:8]) + print pid + sys.exit(0) \ No newline at end of file diff --git a/Kindle_Mobi_Tools/README_Kindle_for_iPad_iPhone_iPodTouch.txt b/Kindle_Mobi_Tools/README_Kindle_for_iPad_iPhone_iPodTouch.txt new file mode 100644 index 0000000..6907448 --- /dev/null +++ b/Kindle_Mobi_Tools/README_Kindle_for_iPad_iPhone_iPodTouch.txt @@ -0,0 +1,29 @@ +Kindle for iPhone, iPod Touch, iPad + + +The Kindle application for iOS (iPhone/iPod Touch/iPad) uses a PID derived from the serial number of the iPhone/iPod Touch/iPad. Kindlepid.py is a python script that turns the serial number into the equivalent PID, which can then be used with the MobiDeDRM script. + +So, to remove the DRM from (Mobipocket) Kindle books downloaded to your iPhone/iPodTouch/iPad, you’ll need the latest toolsvx.x.zip archive and +some way to extract the book files from the backup of your device on your computer. There are several free tools around to do this. + +Double-click on KindlePID.pyw to get your device’s PID, then use the extractor to get your book files, then double-click on MobiDeDRM.pyw with the PID and the files to get Drm-free versions of your books. + +Kindlefix gives you another way to use the PID generated by kindlepid. Some ebook stores and libraries will accept the PIDs generated by kindlepid (some won’t), and you can then download ebooks from the store or library encrypted to work with your Kindle. Except they don’t. There’s a flag byte set in encrypted Kindle ebooks, and Kindles and the Kindle app won’t read encrypted mobipocket ebooks unless that flag is set. Kindlefix will set that flag for you. If your library have Mobipocket ebooks to lend and will accept your Kindle’s PID, you can now check out library ebooks, run kindlefix on them, and then read them on your Kindle, and when your loan period ends, they’ll automatically become unreadable. + +To extract the files from your iPod Touch (not archived Kindle ebooks, but ones actually on your iPod Touch) it’s necessary to first do a back-up of the iPod Touch using iTunes. That creates a backup file on your Mac, and you can then extract the Mobipocket files from that using iPhone/ipod Touch Backup Extractor – free software from here: http://supercrazyawesome.com/ + +Ok, so that will get your the .azw Kindle Mobipocket files. + +Now you need the PID used to encrypt them. To get that you’ll need your iPod Touch UDID number – you can find it in iTunes when your iPod Touch is connected in the Summary page – click on the serial number and it changes to the (40 digit!) UDID. + +And then you need to double-click the KindlePID.pyw script and enter your 40 digit UDID in the window and hit "Start". + + and you should get back a response something like: + +Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky +iPhone serial number (UDID) detected +Mobipocked PID for iPhone serial# FOURTYDIGITUDIDNUMBERGIVENHERE is TENDIGITPID + +which gives you the PID to be used with MobiDeDRM to de-drm the files you extracted. + +All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX and Linux. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. \ No newline at end of file diff --git a/Kindle_Mobi_Tools/REAME_MobiDeDRM.txt b/Kindle_Mobi_Tools/REAME_MobiDeDRM.txt new file mode 100644 index 0000000..f3c6ef7 --- /dev/null +++ b/Kindle_Mobi_Tools/REAME_MobiDeDRM.txt @@ -0,0 +1,6 @@ +- MobiDeDRM.pyw is a simple graphical front end to the mobidedrm.py progam that actually does the DRM removal as long as you know the correct PID to use with it (which depends on the versions of Kindle software used). This is a gui program, simply double-click to launch it. + +PIDCHeck.py is a command line python tools to take an 8 digit PID and add the 2 checksum digits to the end to generate a 10 digit PID + +All of these scripts are python programs. Python 2.X (32 bit) is already installed in Mac OSX and Linux. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. + diff --git a/Topaz_Tools/README_Topaz_Tools.txt b/Topaz_Tools/README_Topaz_Tools.txt new file mode 100644 index 0000000..02bd298 --- /dev/null +++ b/Topaz_Tools/README_Topaz_Tools.txt @@ -0,0 +1,38 @@ +The Topaz Tools only work for "Kindle for PC" books, and original standalone Kindles that have never been updated to firmware 2.5 or later, Kindle for iPhone/iPad/iPodTouch (where the PID is known) and Kindle for Mac (with the PID provided by the Kindle_4_Mac_Tools). + +For Topaz: + +1. Make sure you have Python 2.X installed (32 bit) and properly set as part of your SYSTEM PATH environment variable (On Windows I recommend ActivePython. See their web pages for instructions on how to install and how to properly set your PATH). On Mac OSX 10.6 everything you need is already installed. + +2. Simply download the latest tools_vX.X.zip file (see the comments after the first post on this blog) and extract the entire archive. Do not move or rename anything after extracting the entire archive. + +3. move to tools\Topaz_Tools\ + +4. If you have an old Kindle (never updated to 2.5 or later) or an iPod, iPhone, or iPad or Kindle for Mac and you know your PID then double-click on the following: + +TopazExtract_iPhone_iPad_K4M.pyw + +If you have Kindle for PC (and no Kindle for Mac will NOT work here) then instead double-click on the following: + +TopazExtract_Kindle4PC.pyw + +Hit the first “…” button to select the Topaz book with DRM that you want to convert + +Hit the second “…” to select an entirely new directory to extract the many book pieces into + +And add info for your PID (or extra PIDs) if needed (should not be needed for Kindle For PC). + +Hit the Start button + +3. Next double-click on TopazFiles2SVG.pyw +(use the “…” button to select the new directory you created from the previous step, and hit Start) + +4. Finally double-click on TopazFiles2HTML.pyw +(use the “…” button to slect the new directory you created in step 2, and hit Start) + +5. After all of this you should have book.html inside the directory you created with its own image directory and css style sheet. This file is created from the ocr that is done by Amazon and stored in the Topaz file. All errors belong to Amazon. + +Inside of that same directory, you should have an svg directory which has an exact image of each page of the book. To see it, simply open the .xhtml page which has the embedded svg image in a good browser (Firefox, Safari, etc) + +If you run into any problems – and there can be problems because the format has not been completely reversed engineered, simply copy the entire contents of the Conversion Log window and paste them in a post here or on the Dark Reverser’s New Blog and I will find it and try to help + diff --git a/Topaz_Tools/TopazExtract_KindleV1_iPhone_iPad.pyw b/Topaz_Tools/TopazExtract_iPhone_iPad_K4M.pyw similarity index 100% rename from Topaz_Tools/TopazExtract_KindleV1_iPhone_iPad.pyw rename to Topaz_Tools/TopazExtract_iPhone_iPad_K4M.pyw diff --git a/Topaz_Tools/lib/cmbtc_dump.py b/Topaz_Tools/lib/cmbtc_dump.py index d7cef99..96a75b4 100644 --- a/Topaz_Tools/lib/cmbtc_dump.py +++ b/Topaz_Tools/lib/cmbtc_dump.py @@ -654,12 +654,15 @@ def createDecryptedPayload(payload): if name != "dkey" : ext = '.dat' if name == 'img' : ext = '.jpg' + if name == 'color' : ext = '.jpg' for index in range (0,len(bookHeaderRecords[name])) : fnum = "%04d" % index fname = name + fnum + ext destdir = payload if name == 'img': destdir = os.path.join(payload,'img') + if name == 'color': + destdir = os.path.join(payload,'color_img') if name == 'page': destdir = os.path.join(payload,'page') if name == 'glyphs': @@ -679,6 +682,10 @@ def createDecryptedBook(outdir): if not os.path.exists(destdir): os.makedirs(destdir) + destdir = os.path.join(outdir,'color_img') + if not os.path.exists(destdir): + os.makedirs(destdir) + destdir = os.path.join(outdir,'page') if not os.path.exists(destdir): os.makedirs(destdir) diff --git a/Topaz_Tools/lib/cmbtc_dump_nonK4PC.py b/Topaz_Tools/lib/cmbtc_dump_nonK4PC.py index 0d62404..949fe51 100644 --- a/Topaz_Tools/lib/cmbtc_dump_nonK4PC.py +++ b/Topaz_Tools/lib/cmbtc_dump_nonK4PC.py @@ -360,12 +360,15 @@ def createDecryptedPayload(payload): if name != "dkey" : ext = '.dat' if name == 'img' : ext = '.jpg' + if name == 'color' : ext = '.jpg' for index in range (0,len(bookHeaderRecords[name])) : fnum = "%04d" % index fname = name + fnum + ext destdir = payload if name == 'img': destdir = os.path.join(payload,'img') + if name == 'color': + destdir = os.path.join(payload,'color_img') if name == 'page': destdir = os.path.join(payload,'page') if name == 'glyphs': @@ -385,6 +388,10 @@ def createDecryptedBook(outdir): if not os.path.exists(destdir): os.makedirs(destdir) + destdir = os.path.join(outdir,'color_img') + if not os.path.exists(destdir): + os.makedirs(destdir) + destdir = os.path.join(outdir,'page') if not os.path.exists(destdir): os.makedirs(destdir) diff --git a/eReader_Tools/README_eReaderPDB.txt b/eReader_Tools/README_eReaderPDB.txt new file mode 100644 index 0000000..4d1ae44 --- /dev/null +++ b/eReader_Tools/README_eReaderPDB.txt @@ -0,0 +1,21 @@ +From Apprentice Alf's Blog + +Barnes & Noble/Fictionwise eReader, .pdb + +Barnes & Noble’s .pdb eReader format is one of the survivors from the early days of ebooks. Internally, text is marked up in PML – Palm Markup Language. This makes conversion to other formats more difficult. + +Barnes and Noble .pdb ebooks use a form of Social DRM which requires information on your Credit Card Number (last 8 digits only) and the Name on the Credit card to unencrypt the book that was purchased with that credit card. + +There are three scripts used to decrypt and convert eReader ebooks. + +The first, eReaderPDB2PML.pyw removes the DRM and extracts the PML and images from the ebook into a folder. It’s then possible to use the free DropBook (available for Mac or Windows) to compile the PML back into a DRM-free eReader file. + +The second script, Pml2HTML.pyw, converts the PML file extracted by the first script into xhtml, which can then be converted into your preferred ebook format with a variety of tools. + +The last script is eReaderPDB2PMLZ.pyw and it removes the DRM and extracts the PML and images from the ebook into a zip archive that can be directly imported into Calibre. + +All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users. + +Simply double-click to launch these applications and follow along. + +