From eb7d65bab5df267346bf90e810c4049cb60d2ee6 Mon Sep 17 00:00:00 2001 From: Bipin Date: Mon, 28 Aug 2023 20:17:34 +0530 Subject: [PATCH] Installable from Pip and use in CMD --- .github/workflows/main.yml | 2 +- DeGourou.py => DeGourou/DeGourou.py | 29 +- {decrypt => DeGourou/decrypt}/decodeEPUB.py | 626 +++++++++--------- {decrypt => DeGourou/decrypt}/decodePDF.py | 2 +- {decrypt => DeGourou/decrypt}/params.py | 0 .../decrypt}/zeroedzipinfo.py | 0 {setup => DeGourou/setup}/customRSA.py | 234 +++---- {setup => DeGourou/setup}/data.py | 4 +- {setup => DeGourou/setup}/fulfill.py | 6 +- {setup => DeGourou/setup}/ia.py | 0 {setup => DeGourou/setup}/libadobe.py | 4 +- {setup => DeGourou/setup}/libadobeAccount.py | 14 +- {setup => DeGourou/setup}/libadobeFulfill.py | 10 +- {setup => DeGourou/setup}/libpdf.py | 0 {setup => DeGourou/setup}/loginAccount.py | 6 +- {setup => DeGourou/setup}/params.py | 0 README.md | 17 +- setup.py | 36 + 18 files changed, 520 insertions(+), 470 deletions(-) rename DeGourou.py => DeGourou/DeGourou.py (86%) mode change 100755 => 100644 rename {decrypt => DeGourou/decrypt}/decodeEPUB.py (96%) rename {decrypt => DeGourou/decrypt}/decodePDF.py (99%) rename {decrypt => DeGourou/decrypt}/params.py (100%) rename {decrypt => DeGourou/decrypt}/zeroedzipinfo.py (100%) rename {setup => DeGourou/setup}/customRSA.py (96%) rename {setup => DeGourou/setup}/data.py (99%) rename {setup => DeGourou/setup}/fulfill.py (96%) rename {setup => DeGourou/setup}/ia.py (100%) rename {setup => DeGourou/setup}/libadobe.py (99%) rename {setup => DeGourou/setup}/libadobeAccount.py (98%) rename {setup => DeGourou/setup}/libadobeFulfill.py (98%) rename {setup => DeGourou/setup}/libpdf.py (100%) rename {setup => DeGourou/setup}/loginAccount.py (87%) rename {setup => DeGourou/setup}/params.py (100%) create mode 100644 setup.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a8500f..2db3a5a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: - name: Build Executable uses: Nuitka/Nuitka-Action@main with: - script-name: DeGourou.py + script-name: DeGourou/DeGourou.py onefile: true standalone: true diff --git a/DeGourou.py b/DeGourou/DeGourou.py old mode 100755 new mode 100644 similarity index 86% rename from DeGourou.py rename to DeGourou/DeGourou.py index 215fb74..1ec57a3 --- a/DeGourou.py +++ b/DeGourou/DeGourou.py @@ -1,18 +1,18 @@ #!/usr/bin/env python -from setup.loginAccount import loginAndGetKey -from setup.fulfill import downloadFile +from .setup.loginAccount import loginAndGetKey +from .setup.fulfill import downloadFile -from decrypt.decodePDF import decryptPDF -from decrypt.decodeEPUB import decryptEPUB +from .decrypt.decodePDF import decryptPDF +from .decrypt.decodeEPUB import decryptEPUB import argparse from os import mkdir, remove, rename from os.path import exists -from setup.params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML -from decrypt.params import KEYPATH -from setup.data import createDefaultFiles -from setup.ia import SESSION_FILE, manage_login, get_book, return_book +from .setup.params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML +from .decrypt.params import KEYPATH +from .setup.data import createDefaultFiles +from .setup.ia import SESSION_FILE, manage_login, get_book, return_book def loginADE(email, password): @@ -32,7 +32,7 @@ def loginIA(email,password): manage_login(email,password) print() -def main(acsmFile, outputFilename): +def start(acsmFile, outputFilename): if not exists('account'): mkdir('account') # setting up the account and keys @@ -81,13 +81,13 @@ def handle_IA(url,format): if acsmFile is None: print("Could not get Book, try using ACSm file as input") return - main(acsmFile,None) + start(acsmFile,None) remove(acsmFile) if(return_book(url) is None): print("Please return it yourself") -if __name__ == "__main__": +def main(): parser = argparse.ArgumentParser(description="Download and Decrypt an encrypted PDF or EPUB file.") parser.add_argument("-f", type=str, nargs='?', default=None, help="path to the ACSM file") parser.add_argument("-u", type=str, nargs='?', default=None, help="book url from InternetArchive") @@ -134,8 +134,11 @@ if __name__ == "__main__": elif args.f == None: if exists("URLLink.acsm"): args.f = "URLLink.acsm" - main(args.f, args.o) + start(args.f, args.o) else: parser.print_help() else: - main(args.f, args.o) + start(args.f, args.o) + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/decrypt/decodeEPUB.py b/DeGourou/decrypt/decodeEPUB.py similarity index 96% rename from decrypt/decodeEPUB.py rename to DeGourou/decrypt/decodeEPUB.py index 7c69ad0..7b746e3 100644 --- a/decrypt/decodeEPUB.py +++ b/DeGourou/decrypt/decodeEPUB.py @@ -1,313 +1,313 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ineptepub.py -# Copyright © 2009-2022 by i♥cabbages, Apprentice Harper et al. - -# Released under the terms of the GNU General Public Licence, version 3 -# - -""" -Decrypt Adobe Digital Editions encrypted ePub books. -""" - -from decrypt.params import KEYPATH -__license__ = 'GPL v3' -__version__ = "8.0" - -import sys -import os -import traceback -import base64 -import zlib -from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED -from decrypt.zeroedzipinfo import ZeroedZipInfo -from contextlib import closing -from lxml import etree -from uuid import UUID -import hashlib - -try: - from Cryptodome.Cipher import AES, PKCS1_v1_5 - from Cryptodome.PublicKey import RSA -except ImportError: - from Crypto.Cipher import AES, PKCS1_v1_5 - from Crypto.PublicKey import RSA - - -def unpad(data, padding=16): - if sys.version_info[0] == 2: - pad_len = ord(data[-1]) - else: - pad_len = data[-1] - - return data[:-pad_len] - - -class ADEPTError(Exception): - pass - -class ADEPTNewVersionError(Exception): - pass - -META_NAMES = ('mimetype', 'META-INF/rights.xml') -NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - -class Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES.new(bookkey, AES.MODE_CBC, b'\x00'*16) - self._encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - self._encryptedForceNoDecomp = encryptedForceNoDecomp = set() - self._otherData = otherData = set() - - self._json_elements_to_remove = json_elements_to_remove = set() - self._has_remaining_xml = False - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in self._encryption.findall(expr): - path = elem.get('URI', None) - encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None)) - if path is not None: - if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"): - # Adobe - path = path.encode('utf-8') - encrypted.add(path) - json_elements_to_remove.add(elem.getparent().getparent()) - elif (encryption_type_url == "http://ns.adobe.com/adept/xmlenc#aes128-cbc-uncompressed"): - # Adobe uncompressed, for stuff like video files - path = path.encode('utf-8') - encryptedForceNoDecomp.add(path) - json_elements_to_remove.add(elem.getparent().getparent()) - else: - path = path.encode('utf-8') - otherData.add(path) - self._has_remaining_xml = True - - for elem in json_elements_to_remove: - elem.getparent().remove(elem) - - def check_if_remaining(self): - return self._has_remaining_xml - - def get_xml(self): - return "\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8") - - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - try: - decompressed_bytes = dc.decompress(bytes) - ex = dc.decompress(b'Z') + dc.flush() - if ex: - decompressed_bytes = decompressed_bytes + ex - except: - # possibly not compressed by zip - just return bytes - return bytes - return decompressed_bytes - - def decrypt(self, path, data): - if path.encode('utf-8') in self._encrypted or path.encode('utf-8') in self._encryptedForceNoDecomp: - data = self._aes.decrypt(data)[16:] - if type(data[-1]) != int: - place = ord(data[-1]) - else: - place = data[-1] - data = data[:-place] - if not path.encode('utf-8') in self._encryptedForceNoDecomp: - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def adeptBook(inpath): - 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 False - 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)) - if len(bookkey) in [192, 172, 64]: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def isPassHashBook(inpath): - # If this is an Adobe book, check if it's a PassHash-encrypted book (B&N) - 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 False - 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)) - if len(bookkey) == 64: - return True - except: - pass - - return False - -# Checks the license file and returns the UUID the book is licensed for. -# This is used so that the Calibre plugin can pick the correct decryption key -# first try without having to loop through all possible keys. -def adeptGetUserUUID(inpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - try: - rights = etree.fromstring(inf.read('META-INF/rights.xml')) - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - expr = './/%s' % (adept('user'),) - user_uuid = ''.join(rights.findtext(expr)) - if user_uuid[:9] != "urn:uuid:": - return None - return user_uuid[9:] - except: - return None - -def removeHardening(rights, keytype, keydata): - adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) - textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),))) - - # Gather what we need, and generate the IV - resourceuuid = UUID(textGetter("resource")) - deviceuuid = UUID(textGetter("device")) - fullfillmentuuid = UUID(textGetter("fulfillment")[:36]) - kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes - - # Derive kek from just "keytype" - rem = int(keytype, 10) % 16 - H = hashlib.sha256(keytype.encode("ascii")).digest() - kek = H[2*rem : 16 + rem] + H[rem : 2*rem] - - return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7 - -def decryptBook(userkey, inpath, outpath): - with closing(ZipFile(open(inpath, 'rb'))) as inf: - namelist = inf.namelist() - if 'META-INF/rights.xml' not in namelist or \ - 'META-INF/encryption.xml' not in namelist: - print("{0:s} is DRM-free.".format(os.path.basename(inpath))) - 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'),) - bookkeyelem = rights.find(expr) - bookkey = bookkeyelem.text - keytype = bookkeyelem.attrib.get('keyType', '0') - if len(bookkey) >= 172 and int(keytype, 10) > 2: - print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath))) - elif len(bookkey) == 172: - print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath))) - elif len(bookkey) == 64: - print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath))) - else: - print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath))) - return 1 - - if len(bookkey) != 64: - # Normal or "hardened" Adobe ADEPT - rsakey = RSA.importKey(userkey) # parses the ASN1 structure - bookkey = base64.b64decode(bookkey) - if int(keytype, 10) > 2: - bookkey = removeHardening(rights, keytype, bookkey) - try: - bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads - except ValueError: - bookkey = None - - if bookkey is None: - print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) - return 2 - else: - # Adobe PassHash / B&N - key = base64.b64decode(userkey)[:16] - bookkey = base64.b64decode(bookkey) - bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7 - - if len(bookkey) > 16: - bookkey = bookkey[-16:] - - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey, encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - - for path in (["mimetype"] + namelist): - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - - if path == "mimetype": - zi.compress_type = ZIP_STORED - - elif path == "META-INF/encryption.xml": - # Check if there's still something in there - if (decryptor.check_if_remaining()): - data = decryptor.get_xml() - print("Adding encryption.xml for the remaining embedded files.") - # We removed DRM, but there's still stuff like obfuscated fonts. - else: - continue - - - try: - # get the file info, including time-stamp - oldzi = inf.getinfo(path) - # copy across useful fields - zi.date_time = oldzi.date_time - zi.comment = oldzi.comment - zi.extra = oldzi.extra - zi.internal_attr = oldzi.internal_attr - # external attributes are dependent on the create system, so copy both. - zi.external_attr = oldzi.external_attr - - zi.volume = oldzi.volume - zi.create_system = oldzi.create_system - zi.create_version = oldzi.create_version - - if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): - # If the file name or the comment contains any non-ASCII char, set the UTF8-flag - zi.flag_bits |= 0x800 - except: - pass - - # Python 3 has a bug where the external_attr is reset to `0o600 << 16` - # if it's NULL, so we need a workaround: - if zi.external_attr == 0: - zi = ZeroedZipInfo(zi) - - - if path == "META-INF/encryption.xml": - outf.writestr(zi, data) - else: - outf.writestr(zi, decryptor.decrypt(path, data)) - except: - print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) - return 2 - return 0 - - -def decryptEPUB(inpath): - keypath = KEYPATH - outpath = os.path.basename(inpath).removesuffix(".epub") + "_decrypted.epub" - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print("Successfully decrypted") - return outpath - else: - print("Decryption failed") - return None +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ineptepub.py +# Copyright © 2009-2022 by i♥cabbages, Apprentice Harper et al. + +# Released under the terms of the GNU General Public Licence, version 3 +# + +""" +Decrypt Adobe Digital Editions encrypted ePub books. +""" + +from .params import KEYPATH +__license__ = 'GPL v3' +__version__ = "8.0" + +import sys +import os +import traceback +import base64 +import zlib +from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from .zeroedzipinfo import ZeroedZipInfo +from contextlib import closing +from lxml import etree +from uuid import UUID +import hashlib + +try: + from Cryptodome.Cipher import AES, PKCS1_v1_5 + from Cryptodome.PublicKey import RSA +except ImportError: + from Crypto.Cipher import AES, PKCS1_v1_5 + from Crypto.PublicKey import RSA + + +def unpad(data, padding=16): + if sys.version_info[0] == 2: + pad_len = ord(data[-1]) + else: + pad_len = data[-1] + + return data[:-pad_len] + + +class ADEPTError(Exception): + pass + +class ADEPTNewVersionError(Exception): + pass + +META_NAMES = ('mimetype', 'META-INF/rights.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES.new(bookkey, AES.MODE_CBC, b'\x00'*16) + self._encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + self._encryptedForceNoDecomp = encryptedForceNoDecomp = set() + self._otherData = otherData = set() + + self._json_elements_to_remove = json_elements_to_remove = set() + self._has_remaining_xml = False + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in self._encryption.findall(expr): + path = elem.get('URI', None) + encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None)) + if path is not None: + if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"): + # Adobe + path = path.encode('utf-8') + encrypted.add(path) + json_elements_to_remove.add(elem.getparent().getparent()) + elif (encryption_type_url == "http://ns.adobe.com/adept/xmlenc#aes128-cbc-uncompressed"): + # Adobe uncompressed, for stuff like video files + path = path.encode('utf-8') + encryptedForceNoDecomp.add(path) + json_elements_to_remove.add(elem.getparent().getparent()) + else: + path = path.encode('utf-8') + otherData.add(path) + self._has_remaining_xml = True + + for elem in json_elements_to_remove: + elem.getparent().remove(elem) + + def check_if_remaining(self): + return self._has_remaining_xml + + def get_xml(self): + return "\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8") + + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + try: + decompressed_bytes = dc.decompress(bytes) + ex = dc.decompress(b'Z') + dc.flush() + if ex: + decompressed_bytes = decompressed_bytes + ex + except: + # possibly not compressed by zip - just return bytes + return bytes + return decompressed_bytes + + def decrypt(self, path, data): + if path.encode('utf-8') in self._encrypted or path.encode('utf-8') in self._encryptedForceNoDecomp: + data = self._aes.decrypt(data)[16:] + if type(data[-1]) != int: + place = ord(data[-1]) + else: + place = data[-1] + data = data[:-place] + if not path.encode('utf-8') in self._encryptedForceNoDecomp: + data = self.decompress(data) + return data + +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def adeptBook(inpath): + 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 False + 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)) + if len(bookkey) in [192, 172, 64]: + return True + except: + # if we couldn't check, assume it is + return True + return False + +def isPassHashBook(inpath): + # If this is an Adobe book, check if it's a PassHash-encrypted book (B&N) + 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 False + 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)) + if len(bookkey) == 64: + return True + except: + pass + + return False + +# Checks the license file and returns the UUID the book is licensed for. +# This is used so that the Calibre plugin can pick the correct decryption key +# first try without having to loop through all possible keys. +def adeptGetUserUUID(inpath): + with closing(ZipFile(open(inpath, 'rb'))) as inf: + try: + rights = etree.fromstring(inf.read('META-INF/rights.xml')) + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + expr = './/%s' % (adept('user'),) + user_uuid = ''.join(rights.findtext(expr)) + if user_uuid[:9] != "urn:uuid:": + return None + return user_uuid[9:] + except: + return None + +def removeHardening(rights, keytype, keydata): + adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) + textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),))) + + # Gather what we need, and generate the IV + resourceuuid = UUID(textGetter("resource")) + deviceuuid = UUID(textGetter("device")) + fullfillmentuuid = UUID(textGetter("fulfillment")[:36]) + kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes + + # Derive kek from just "keytype" + rem = int(keytype, 10) % 16 + H = hashlib.sha256(keytype.encode("ascii")).digest() + kek = H[2*rem : 16 + rem] + H[rem : 2*rem] + + return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7 + +def decryptBook(userkey, inpath, outpath): + with closing(ZipFile(open(inpath, 'rb'))) as inf: + namelist = inf.namelist() + if 'META-INF/rights.xml' not in namelist or \ + 'META-INF/encryption.xml' not in namelist: + print("{0:s} is DRM-free.".format(os.path.basename(inpath))) + 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'),) + bookkeyelem = rights.find(expr) + bookkey = bookkeyelem.text + keytype = bookkeyelem.attrib.get('keyType', '0') + if len(bookkey) >= 172 and int(keytype, 10) > 2: + print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath))) + elif len(bookkey) == 172: + print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath))) + elif len(bookkey) == 64: + print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath))) + else: + print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath))) + return 1 + + if len(bookkey) != 64: + # Normal or "hardened" Adobe ADEPT + rsakey = RSA.importKey(userkey) # parses the ASN1 structure + bookkey = base64.b64decode(bookkey) + if int(keytype, 10) > 2: + bookkey = removeHardening(rights, keytype, bookkey) + try: + bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads + except ValueError: + bookkey = None + + if bookkey is None: + print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) + return 2 + else: + # Adobe PassHash / B&N + key = base64.b64decode(userkey)[:16] + bookkey = base64.b64decode(bookkey) + bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7 + + if len(bookkey) > 16: + bookkey = bookkey[-16:] + + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey, encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + + for path in (["mimetype"] + namelist): + data = inf.read(path) + zi = ZipInfo(path) + zi.compress_type=ZIP_DEFLATED + + if path == "mimetype": + zi.compress_type = ZIP_STORED + + elif path == "META-INF/encryption.xml": + # Check if there's still something in there + if (decryptor.check_if_remaining()): + data = decryptor.get_xml() + print("Adding encryption.xml for the remaining embedded files.") + # We removed DRM, but there's still stuff like obfuscated fonts. + else: + continue + + + try: + # get the file info, including time-stamp + oldzi = inf.getinfo(path) + # copy across useful fields + zi.date_time = oldzi.date_time + zi.comment = oldzi.comment + zi.extra = oldzi.extra + zi.internal_attr = oldzi.internal_attr + # external attributes are dependent on the create system, so copy both. + zi.external_attr = oldzi.external_attr + + zi.volume = oldzi.volume + zi.create_system = oldzi.create_system + zi.create_version = oldzi.create_version + + if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): + # If the file name or the comment contains any non-ASCII char, set the UTF8-flag + zi.flag_bits |= 0x800 + except: + pass + + # Python 3 has a bug where the external_attr is reset to `0o600 << 16` + # if it's NULL, so we need a workaround: + if zi.external_attr == 0: + zi = ZeroedZipInfo(zi) + + + if path == "META-INF/encryption.xml": + outf.writestr(zi, data) + else: + outf.writestr(zi, decryptor.decrypt(path, data)) + except: + print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) + return 2 + return 0 + + +def decryptEPUB(inpath): + keypath = KEYPATH + outpath = os.path.basename(inpath).removesuffix(".epub") + "_decrypted.epub" + userkey = open(keypath,'rb').read() + result = decryptBook(userkey, inpath, outpath) + if result == 0: + print("Successfully decrypted") + return outpath + else: + print("Decryption failed") + return None diff --git a/decrypt/decodePDF.py b/DeGourou/decrypt/decodePDF.py similarity index 99% rename from decrypt/decodePDF.py rename to DeGourou/decrypt/decodePDF.py index 271f859..0838aaf 100644 --- a/decrypt/decodePDF.py +++ b/DeGourou/decrypt/decodePDF.py @@ -13,7 +13,7 @@ Decrypts Adobe ADEPT-encrypted PDF files. """ -from decrypt.params import KEYPATH +from .params import KEYPATH __license__ = 'GPL v3' __version__ = "10.0.4" diff --git a/decrypt/params.py b/DeGourou/decrypt/params.py similarity index 100% rename from decrypt/params.py rename to DeGourou/decrypt/params.py diff --git a/decrypt/zeroedzipinfo.py b/DeGourou/decrypt/zeroedzipinfo.py similarity index 100% rename from decrypt/zeroedzipinfo.py rename to DeGourou/decrypt/zeroedzipinfo.py diff --git a/setup/customRSA.py b/DeGourou/setup/customRSA.py similarity index 96% rename from setup/customRSA.py rename to DeGourou/setup/customRSA.py index 3c3597b..d394bf0 100644 --- a/setup/customRSA.py +++ b/DeGourou/setup/customRSA.py @@ -1,118 +1,118 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -''' -Use my own small RSA code so we don't have to include the huge -python3-rsa just for these small bits. -The original code used blinding and this one doesn't, -but we don't really care about side-channel attacks ... -''' - -import sys - -try: - from Cryptodome.PublicKey import RSA -except ImportError: - # Some distros still ship this as Crypto - from Crypto.PublicKey import RSA - -class CustomRSA: - - @staticmethod - def encrypt_for_adobe_signature(signing_key, message): - key = RSA.importKey(signing_key) - keylen = CustomRSA.byte_size(key.n) - padded = CustomRSA.pad_message(message, keylen) - payload = CustomRSA.transform_bytes2int(padded) - encrypted = CustomRSA.normal_encrypt(key, payload) - block = CustomRSA.transform_int2bytes(encrypted, keylen) - return bytearray(block) - - @staticmethod - def byte_size(number): - # type: (int) -> int - return (number.bit_length() + 7) // 8 - - @staticmethod - def pad_message(message, target_len): - # type: (bytes, int) -> bytes - - # Padding always uses 0xFF - # Returns: 00 01 PADDING 00 MESSAGE - - max_message_length = target_len - 11 - message_length = len(message) - - if message_length > max_message_length: - raise OverflowError("Message too long, has %d bytes but only space for %d" % (message_length, max_message_length)) - - padding_len = target_len - message_length - 3 - - ret = bytearray(b"".join([b"\x00\x01", padding_len * b"\xff", b"\x00"])) - ret.extend(bytes(message)) - - return ret - - @staticmethod - def normal_encrypt(key, message): - - if message < 0 or message > key.n: - raise ValueError("Invalid message") - - encrypted = pow(message, key.d, key.n) - return encrypted - - @staticmethod - def py2_int_to_bytes(value, length, big_endian = True): - result = [] - - for i in range(0, length): - result.append(value >> (i * 8) & 0xff) - - if big_endian: - result.reverse() - - return result - - @staticmethod - def py2_bytes_to_int(bytes, big_endian = True): - # type: (bytes, bool) -> int - - my_bytes = bytes - if not big_endian: - my_bytes.reverse() - - result = 0 - for b in my_bytes: - result = result * 256 + int(b) - - return result - - @staticmethod - def transform_bytes2int(raw_bytes): - # type: (bytes) -> int - - if sys.version_info[0] >= 3: - return int.from_bytes(raw_bytes, "big", signed=False) - - return CustomRSA.py2_bytes_to_int(raw_bytes, True) - - - @staticmethod - def transform_int2bytes(number, fill_size = 0): - # type: (int, int) -> bytes - - if number < 0: - raise ValueError("Negative number") - - size = None - - if fill_size > 0: - size = fill_size - else: - size = max(1, CustomRSA.byte_size(number)) - - if sys.version_info[0] >= 3: - return number.to_bytes(size, "big") - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +''' +Use my own small RSA code so we don't have to include the huge +python3-rsa just for these small bits. +The original code used blinding and this one doesn't, +but we don't really care about side-channel attacks ... +''' + +import sys + +try: + from Cryptodome.PublicKey import RSA +except ImportError: + # Some distros still ship this as Crypto + from Crypto.PublicKey import RSA + +class CustomRSA: + + @staticmethod + def encrypt_for_adobe_signature(signing_key, message): + key = RSA.importKey(signing_key) + keylen = CustomRSA.byte_size(key.n) + padded = CustomRSA.pad_message(message, keylen) + payload = CustomRSA.transform_bytes2int(padded) + encrypted = CustomRSA.normal_encrypt(key, payload) + block = CustomRSA.transform_int2bytes(encrypted, keylen) + return bytearray(block) + + @staticmethod + def byte_size(number): + # type: (int) -> int + return (number.bit_length() + 7) // 8 + + @staticmethod + def pad_message(message, target_len): + # type: (bytes, int) -> bytes + + # Padding always uses 0xFF + # Returns: 00 01 PADDING 00 MESSAGE + + max_message_length = target_len - 11 + message_length = len(message) + + if message_length > max_message_length: + raise OverflowError("Message too long, has %d bytes but only space for %d" % (message_length, max_message_length)) + + padding_len = target_len - message_length - 3 + + ret = bytearray(b"".join([b"\x00\x01", padding_len * b"\xff", b"\x00"])) + ret.extend(bytes(message)) + + return ret + + @staticmethod + def normal_encrypt(key, message): + + if message < 0 or message > key.n: + raise ValueError("Invalid message") + + encrypted = pow(message, key.d, key.n) + return encrypted + + @staticmethod + def py2_int_to_bytes(value, length, big_endian = True): + result = [] + + for i in range(0, length): + result.append(value >> (i * 8) & 0xff) + + if big_endian: + result.reverse() + + return result + + @staticmethod + def py2_bytes_to_int(bytes, big_endian = True): + # type: (bytes, bool) -> int + + my_bytes = bytes + if not big_endian: + my_bytes.reverse() + + result = 0 + for b in my_bytes: + result = result * 256 + int(b) + + return result + + @staticmethod + def transform_bytes2int(raw_bytes): + # type: (bytes) -> int + + if sys.version_info[0] >= 3: + return int.from_bytes(raw_bytes, "big", signed=False) + + return CustomRSA.py2_bytes_to_int(raw_bytes, True) + + + @staticmethod + def transform_int2bytes(number, fill_size = 0): + # type: (int, int) -> bytes + + if number < 0: + raise ValueError("Negative number") + + size = None + + if fill_size > 0: + size = fill_size + else: + size = max(1, CustomRSA.byte_size(number)) + + if sys.version_info[0] >= 3: + return number.to_bytes(size, "big") + return CustomRSA.py2_int_to_bytes(number, size, True) \ No newline at end of file diff --git a/setup/data.py b/DeGourou/setup/data.py similarity index 99% rename from setup/data.py rename to DeGourou/setup/data.py index 9aa457c..a44681d 100644 --- a/setup/data.py +++ b/DeGourou/setup/data.py @@ -1,5 +1,5 @@ -from setup.params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML -from decrypt.params import KEYPATH +from .params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML +from ..decrypt.params import KEYPATH keyContent = b'0\x82\x02\\\x02\x01\x00\x02\x81\x81\x00\xad*E\x8e0\n\x91\xd6\xbaj\xc1t3\xc2R2h\xa6\x18\x063i\xfd\x9bR/e\xa6\xec\x87\xab\x11\n\'\xb7\x93\x14\xb6\xbbm\xfa\xf0\xf4\xe8=\x18\xa6\xe9\x15$\xdao\xb3\x8d\xf5\xddT\n\xf5\t<\xe8\xb2\x93k\x02zi\xe6\x86\x10F\x13\xc9m\xcfZ\x83\xe6=\xd6G\xf2/]3\xff\x8ch#\xea|\xa9I\x9a\xf6\xbf\x19\xd9\x10\xe0\x18\xa1\rb\x801k~\xc03f\x84\x07{v\x88\x18\x9bH\x91+o \x90\x9b\xb7\xf5\x02\x03\x01\x00\x01\x02\x81\x80\x05\xfd\x95\xd3\x886\x9a\xba\x8ck\xc1\xb5\xc21\x86\xab\x1a\xa8^\x1af%\x9b\x8a\xc0\x96\xc6\x10}\xb6\xf6\xeb\x80\xc4R\xc2@\x9d\xf9F\xa1\xf7\xe6\x06jPs\xad\xc3w\xd3\xea\xb7\xca\xec\x03\x17\xcf\xff\x01u\x96\x15\n\x0e&\xb0\xc7\x90F\xc4\xdaZ"\xc1)>\xee\x19\xf6\x05\xa5\xba\x00H)\xa8>\x1fC\x02\xd3\xba\xa8){\x06^D\xb4\xfd"\x05\x05\xec\xef\xdb.tbZ8\xabU<,+\xb6\xfaI\x98\xcc7H\xedr\xa9\xfd\x02A\x00\xc27%\xc5\xa0\xff\xd5l\xaa\x7f=\x1dx\xab?\xd8~\xf7v\x1f!\x0cCh\xc9\xb4\x1a\x8b\xb2\xaeC\xa0\xf9\x91\xcc\x99<\x11\xfbQ\xae\x8fG\xb0\xd1b\x0c=\xebR\x19\xb4\x15\xd4\x1c\xbe\xf4\xc7E\xe8\xea\xe1\xb3\x0b\x02A\x00\xe4@\xcb(\xdd\x04F\xe4jT\xe5a\xaaj\xaf=F\xa1\xaf\x1c\xa6F\x93\xc7V1\xd9\xb1\x96\xdb\x1b\xf5\x86\r\xb11\x10\x12\x18\xc5\xee\xaeD\xa3\xc1/\xe3\xf2\x8f\xaf\xad\xda\xe6\t\x8d\x9d\x99z\x04\xeeK\xdb \xff\x02A\x00\xad_\x9d\x90v\xd0\xeb->f\xa7\xa0\x0f\x80\x90V+\xc1\xac\xe8\xcd\x0f\xad}u\xd2\x19\x80k\xd9\xb4\xf5\x96\xd4\xd8\xd8R\x0f\x9bR\xa7\x89\xb0m\xdf\xfc\xaf\x00\xf7y+\x08\xe0\x13\xa25\xb5=\xce\xe2\xc6\x0b\x05Q\x02@\x18\xee\xf7\x02\\\xbaU\xe0\'\xb9da9\xd3s\x97\x16\xfb\x1c|\xdd\xb1\x01\xfd\x99m\xd2\xa0\xf2\xa0\xb6\xba(M\xa0\x98\x82o\xe7\xa2\xdf\x82\xcb\xde\xb3\x80\xbe\xbe\xc5qdep\x11\x85\x15\xbd)6\x16\xad\xd4\x9f\x13\x02@\x0f\x15\xc1Y"b\x19\x81Q\x81\x8d\x006\xe4\xf0e\xa2\xa7\xb8\x98{\x1c\x12\xe0\nw\xbe\x86A-\xd0\x1c7\xf3\x169\xadd3\x85\xaf\x13\x99\x08\x97e)c\xaf\xb1V\xf1\x15\xf6K\r\x16\xb4\xf9\xd1\x10\xe2\x92\xf9' diff --git a/setup/fulfill.py b/DeGourou/setup/fulfill.py similarity index 96% rename from setup/fulfill.py rename to DeGourou/setup/fulfill.py index 15a99a5..dcd4fd0 100644 --- a/setup/fulfill.py +++ b/DeGourou/setup/fulfill.py @@ -12,9 +12,9 @@ import os, time, shutil import zipfile from lxml import etree -from setup.libadobe import sendHTTPRequest_DL2FILE -from setup.libadobeFulfill import buildRights, fulfill -from setup.libpdf import patch_drm_into_pdf +from .libadobe import sendHTTPRequest_DL2FILE +from .libadobeFulfill import buildRights, fulfill +from .libpdf import patch_drm_into_pdf ####################################################################### diff --git a/setup/ia.py b/DeGourou/setup/ia.py similarity index 100% rename from setup/ia.py rename to DeGourou/setup/ia.py diff --git a/setup/libadobe.py b/DeGourou/setup/libadobe.py similarity index 99% rename from setup/libadobe.py rename to DeGourou/setup/libadobe.py index a02a368..ccd4a65 100644 --- a/setup/libadobe.py +++ b/DeGourou/setup/libadobe.py @@ -30,7 +30,7 @@ except ImportError: #@@CALIBRE_COMPAT_CODE@@ -from setup.customRSA import CustomRSA +from .customRSA import CustomRSA from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates from cryptography.hazmat.primitives import serialization @@ -38,7 +38,7 @@ from cryptography.hazmat.primitives import serialization VAR_ACS_SERVER_HTTP = "http://adeactivate.adobe.com/adept" VAR_ACS_SERVER_HTTPS = "https://adeactivate.adobe.com/adept" -from setup.params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML +from .params import FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML # Lists of different ADE "versions" we know about VAR_VER_SUPP_CONFIG_NAMES = [ "ADE 1.7.2", "ADE 2.0.1", "ADE 3.0.1", "ADE 4.0.3", "ADE 4.5.10", "ADE 4.5.11" ] diff --git a/setup/libadobeAccount.py b/DeGourou/setup/libadobeAccount.py similarity index 98% rename from setup/libadobeAccount.py rename to DeGourou/setup/libadobeAccount.py index be604ef..47d4b4f 100644 --- a/setup/libadobeAccount.py +++ b/DeGourou/setup/libadobeAccount.py @@ -15,12 +15,12 @@ except ImportError: #@@CALIBRE_COMPAT_CODE@@ -from setup.libadobe import addNonce, sign_node, sendRequestDocu, sendHTTPRequest -from setup.libadobe import makeFingerprint, makeSerial, encrypt_with_device_key, decrypt_with_device_key -from setup.libadobe import get_devkey_path, get_device_path, get_activation_xml_path -from setup.libadobe import VAR_VER_SUPP_CONFIG_NAMES, VAR_VER_HOBBES_VERSIONS, VAR_VER_OS_IDENTIFIERS -from setup.libadobe import VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO, VAR_VER_SUPP_VERSIONS, VAR_ACS_SERVER_HTTP -from setup.libadobe import VAR_ACS_SERVER_HTTPS, VAR_VER_BUILD_IDS, VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT, VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE +from .libadobe import addNonce, sign_node, sendRequestDocu, sendHTTPRequest +from .libadobe import makeFingerprint, makeSerial, encrypt_with_device_key, decrypt_with_device_key +from .libadobe import get_devkey_path, get_device_path, get_activation_xml_path +from .libadobe import VAR_VER_SUPP_CONFIG_NAMES, VAR_VER_HOBBES_VERSIONS, VAR_VER_OS_IDENTIFIERS +from .libadobe import VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO, VAR_VER_SUPP_VERSIONS, VAR_ACS_SERVER_HTTP +from .libadobe import VAR_ACS_SERVER_HTTPS, VAR_VER_BUILD_IDS, VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT, VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE def createDeviceFile(randomSerial, useVersionIndex = 0): @@ -213,7 +213,7 @@ def createUser(useVersionIndex = 0, authCert = None): def encryptLoginCredentials(username, password, authenticationCertificate): # type: (str, str, str) -> bytes - from setup.libadobe import devkey_bytes as devkey_adobe + from .libadobe import devkey_bytes as devkey_adobe import struct if devkey_adobe is not None: diff --git a/setup/libadobeFulfill.py b/DeGourou/setup/libadobeFulfill.py similarity index 98% rename from setup/libadobeFulfill.py rename to DeGourou/setup/libadobeFulfill.py index 2d2c57e..d4bd7fd 100644 --- a/setup/libadobeFulfill.py +++ b/DeGourou/setup/libadobeFulfill.py @@ -5,10 +5,10 @@ import time #@@CALIBRE_COMPAT_CODE@@ -from setup.libadobe import addNonce, sign_node, get_cert_from_pkcs12, sendRequestDocu, sendRequestDocuRC, sendHTTPRequest -from setup.libadobe import get_devkey_path, get_device_path, get_activation_xml_path -from setup.libadobe import VAR_VER_SUPP_VERSIONS, VAR_VER_HOBBES_VERSIONS -from setup.libadobe import VAR_VER_BUILD_IDS, VAR_VER_USE_DIFFERENT_NOTIFICATION_XML_ORDER +from .libadobe import addNonce, sign_node, get_cert_from_pkcs12, sendRequestDocu, sendRequestDocuRC, sendHTTPRequest +from .libadobe import get_devkey_path, get_device_path, get_activation_xml_path +from .libadobe import VAR_VER_SUPP_VERSIONS, VAR_VER_HOBBES_VERSIONS +from .libadobe import VAR_VER_BUILD_IDS, VAR_VER_USE_DIFFERENT_NOTIFICATION_XML_ORDER def buildFulfillRequest(acsm): @@ -143,7 +143,7 @@ def getDecryptedCert(pkcs12_b64_string = None): pkcs12_data = base64.b64decode(pkcs12_b64_string) try: - from setup.libadobe import devkey_bytes as devkey_adobe + from .libadobe import devkey_bytes as devkey_adobe except: pass diff --git a/setup/libpdf.py b/DeGourou/setup/libpdf.py similarity index 100% rename from setup/libpdf.py rename to DeGourou/setup/libpdf.py diff --git a/setup/loginAccount.py b/DeGourou/setup/loginAccount.py similarity index 87% rename from setup/loginAccount.py rename to DeGourou/setup/loginAccount.py index d1715c5..dbb9610 100644 --- a/setup/loginAccount.py +++ b/DeGourou/setup/loginAccount.py @@ -5,14 +5,14 @@ This is an experimental Python version of libgourou. ''' -from setup.libadobe import createDeviceKeyFile, FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML -from setup.libadobeAccount import createDeviceFile, createUser, signIn, activateDevice, exportAccountEncryptionKeyDER, getAccountUUID +from .libadobe import createDeviceKeyFile, FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML +from .libadobeAccount import createDeviceFile, createUser, signIn, activateDevice, exportAccountEncryptionKeyDER, getAccountUUID from os.path import exists VAR_MAIL = "" VAR_PASS = "" VAR_VER = 1 # None # 1 for ADE2.0.1, 2 for ADE3.0.1 -from decrypt.params import KEYPATH +from ..decrypt.params import KEYPATH ################################################################# diff --git a/setup/params.py b/DeGourou/setup/params.py similarity index 100% rename from setup/params.py rename to DeGourou/setup/params.py diff --git a/README.md b/README.md index 3d2eb40..f303503 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This tool is intended for educational purposes only. Its primary aim is to assis ## Usage ``` -usage: DeGourou.py [-h] [-f [F]] [-u [U]] [-t [T]] [-o [O]] [-la] [-li] [-e [E]] [-p [P]] [-lo] +usage: DeGourou [-h] [-f [F]] [-u [U]] [-t [T]] [-o [O]] [-la] [-li] [-e [E]] [-p [P]] [-lo] Download and Decrypt an encrypted PDF or EPUB file. @@ -74,17 +74,28 @@ optional arguments: C. MacOS user's accordingly with name ```DeGourou.bin``` +### For Middlemans + +1. Install through Pip using + + ``` + pip install git+https://gitea.com/bipinkrish/DeGourou.git + ``` + +2. Use `degourou` in Terminal/CMD + ### For Developers 1. Clone the repositary or Download zip file and extract it 2. Install requirements using pip -3. Run "DeGourou.py" file +3. Run "DeGourou.py" file in "DeGourou" directory ``` -git clone https://github.com/bipinkrish/DeGourou.git +git clone https://gitea.com/bipinkrish/DeGourou.git cd DeGourou pip install -r requirements.txt +cd DeGourou python DeGourou.py ``` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..85ec2bc --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +from setuptools import setup + +setup( + name='DeGourou', + version='1.3.8', + description='Automate the process of getting decrypted ebook from InternetArchive without the need for Adobe Digital Editions and Calibre.', + url='https://gitea.com/bipinkrish/DeGourou', + author='Bipin krishna', + license='GPL3', + packages=['DeGourou',"DeGourou/setup","DeGourou/decrypt"], + install_requires=['pycryptodomex>=3.17', + 'cryptography>=41.0.1', + 'lxml>=4.9.2', + 'requests>=2.31.0', + 'charset-normalizer>=3.1.0' + ], + + entry_points={ + 'console_scripts': [ + 'degourou = DeGourou.DeGourou:main' + ] + }, + + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Operating System :: MacOS', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], +) \ No newline at end of file