diff options
author | NoDRM <[email protected]> | 2021-12-29 09:14:35 +0100 |
---|---|---|
committer | NoDRM <[email protected]> | 2021-12-29 09:14:35 +0100 |
commit | 9c40b3ce5a0e2d3f0c74e6a816947a81778a3c4b (patch) | |
tree | f875eff55d036573d1982b40492533735507d682 | |
parent | 80f511ade93cebbf93a3d32bb1f7e8ab64335dd5 (diff) |
Cleanup
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | CALIBRE_CLI_INSTRUCTIONS.md | 6 | ||||
-rw-r--r-- | DeDRM_plugin/DeDRM_Help.htm | 2 | ||||
-rw-r--r-- | DeDRM_plugin/__init__.py | 4 | ||||
-rw-r--r-- | DeDRM_plugin/_unused_activitybar.py (renamed from DeDRM_plugin/activitybar.py) | 2 | ||||
-rw-r--r-- | DeDRM_plugin/_unused_scrolltextwidget.py (renamed from DeDRM_plugin/scrolltextwidget.py) | 3 | ||||
-rw-r--r-- | DeDRM_plugin/ignobleepub.py | 448 | ||||
-rw-r--r-- | DeDRM_plugin/scriptinterface.py | 12 | ||||
-rw-r--r-- | DeDRM_plugin/topazextract.py | 4 |
9 files changed, 24 insertions, 463 deletions
@@ -2,4 +2,8 @@ .DS_Store # local test data -/user_data/
\ No newline at end of file +/user_data/ + +# Cache +/DeDRM_plugin/__pycache__ +/DeDRM_plugin/standalone/__pycache__
\ No newline at end of file diff --git a/CALIBRE_CLI_INSTRUCTIONS.md b/CALIBRE_CLI_INSTRUCTIONS.md index 9c1359f..0d7dbd4 100644 --- a/CALIBRE_CLI_INSTRUCTIONS.md +++ b/CALIBRE_CLI_INSTRUCTIONS.md @@ -13,16 +13,16 @@ platforms. #### Install plugins - Download the DeDRM `.zip` archive from DeDRM_tools' - [latest release](https://github.com/apprenticeharper/DeDRM_tools/releases/latest). + [latest release](https://github.com/noDRM/DeDRM_tools/releases/latest). Then unzip it. - Add the DeDRM plugin to Calibre: ``` cd *the unzipped DeDRM_tools folder* - calibre-customize --add DeDRM_calibre_plugin/DeDRM_plugin.zip + calibre-customize --add DeDRM_plugin.zip ``` - Add the Obok plugin: ``` - calibre-customize --add Obok_calibre_plugin/obok_plugin.zip + calibre-customize --add Obok_plugin.zip ``` #### Enter your keys diff --git a/DeDRM_plugin/DeDRM_Help.htm b/DeDRM_plugin/DeDRM_Help.htm index 6ec385c..c5a92ac 100644 --- a/DeDRM_plugin/DeDRM_Help.htm +++ b/DeDRM_plugin/DeDRM_Help.htm @@ -46,7 +46,7 @@ p {margin-top: 0} <h3>Credits:</h3> <ul> -<li>NoDRM for a bunch of updates and the Readium LCP support</li> +<li>NoDRM for a bunch of updates and maintenance since November 2021, and the Readium LCP support</li> <li>The Dark Reverser for the Mobipocket and eReader scripts</li> <li>i♥cabbages for the Adobe Digital Editions scripts</li> <li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li> diff --git a/DeDRM_plugin/__init__.py b/DeDRM_plugin/__init__.py index b0a5e4b..ab9bcf7 100644 --- a/DeDRM_plugin/__init__.py +++ b/DeDRM_plugin/__init__.py @@ -994,11 +994,11 @@ class DeDRM(FileTypePlugin): decrypted_ebook = self.eReaderDecrypt(path_to_ebook) pass elif booktype == 'pdf': - # Adobe PDF (hopefully) + # Adobe PDF (hopefully) or LCP PDF decrypted_ebook = self.PDFDecrypt(path_to_ebook) pass elif booktype == 'epub': - # Adobe Adept or B&N ePub + # Adobe Adept, PassHash (B&N) or LCP ePub decrypted_ebook = self.ePubDecrypt(path_to_ebook) else: print("Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)) diff --git a/DeDRM_plugin/activitybar.py b/DeDRM_plugin/_unused_activitybar.py index bec991a..8ebc10c 100644 --- a/DeDRM_plugin/activitybar.py +++ b/DeDRM_plugin/_unused_activitybar.py @@ -1,3 +1,5 @@ +# I think this file is unused? + import sys import tkinter import tkinter.constants diff --git a/DeDRM_plugin/scrolltextwidget.py b/DeDRM_plugin/_unused_scrolltextwidget.py index c95a264..5969ea1 100644 --- a/DeDRM_plugin/scrolltextwidget.py +++ b/DeDRM_plugin/_unused_scrolltextwidget.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# I think this file is unused? + + import tkinter import tkinter.constants diff --git a/DeDRM_plugin/ignobleepub.py b/DeDRM_plugin/ignobleepub.py deleted file mode 100644 index e1cd88f..0000000 --- a/DeDRM_plugin/ignobleepub.py +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ignobleepub.py -# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. - -# Released under the terms of the GNU General Public Licence, version 3 -# <http://www.gnu.org/licenses/> - -# -# Revision history: -# 1 - Initial release -# 2 - Added OS X support by using OpenSSL when available -# 3 - screen out improper key lengths to prevent segfaults on Linux -# 3.1 - Allow Windows versions of libcrypto to be found -# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first, OpenSSL next -# 3.4 - Modify interface to allow use with import -# 3.5 - Fix for potential problem with PyCrypto -# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code -# 3.7 - Tweaked to match ineptepub more closely -# 3.8 - Fixed to retain zip file metadata (e.g. file modification date) -# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility -# 4.0 - Work if TkInter is missing -# 4.1 - Import tkFileDialog, don't assume something else will import it. -# 5.0 - Python 3 for calibre 5.0 - -""" -Decrypt Barnes & Noble encrypted ePub books. -""" - -__license__ = 'GPL v3' -__version__ = "5.0" - -import sys -import os -import traceback -import base64 -import zlib -import zipfile -from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree - -# Wrap a stream so that output gets flushed immediately -# and also make sure that any unicode strings get -# encoded using "replace" before writing them. -class SafeUnbuffered: - def __init__(self, stream): - self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" - def write(self, data): - if isinstance(data,str) or isinstance(data,unicode): - # str for Python3, unicode for Python2 - data = data.encode(self.encoding,"replace") - try: - buffer = getattr(self.stream, 'buffer', self.stream) - # self.stream.buffer for Python3, self.stream for Python2 - buffer.write(data) - buffer.flush() - except: - # We can do nothing if a write fails - raise - def __getattr__(self, attr): - return getattr(self.stream, attr) - -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - range(start, argc.value)] - return ["ineptepub.py"] - else: - argvencoding = sys.stdin.encoding or "utf-8" - return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv] - - -class IGNOBLEError(Exception): - pass - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_set_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 = (b'\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, b'\x00'*16) - - def decrypt(self, data): - return self._aes.decrypt(data) - - return AES - -def _load_crypto(): - AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - -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 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: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress(b'Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if bytes(path,'utf-8') in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-data[-1]] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def ignobleBook(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) == 64: - return True - except: - # if we couldn't check, assume it is - return True - return False - -def decryptBook(keyb64, inpath, outpath): - if AES is None: - raise IGNOBLEError("PyCrypto or OpenSSL must be installed.") - key = base64.b64decode(keyb64)[: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: - 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'),) - bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) != 64: - print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath))) - return 1 - bookkey = aes.decrypt(base64.b64decode(bookkey)) - bookkey = bookkey[:-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') - zi.compress_type=ZIP_STORED - try: - # if the mimetype is present, get its info, including time-stamp - oldzi = inf.getinfo('mimetype') - # copy across fields to be preserved - 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.create_system = oldzi.create_system - except: - pass - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - zi = ZipInfo(path) - zi.compress_type=ZIP_DEFLATED - 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.create_system = oldzi.create_system - except: - pass - 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 cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print("usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname)) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) - return result - -def gui_main(): - try: - import tkinter - import tkinter.constants - import tkinter.filedialog - import tkinter.messagebox - import traceback - except: - return cli_main() - - class DecryptionDialog(tkinter.Frame): - def __init__(self, root): - tkinter.Frame.__init__(self, root, border=5) - self.status = tkinter.Label(self, text="Select files for decryption") - self.status.pack(fill=tkinter.constants.X, expand=1) - body = tkinter.Frame(self) - body.pack(fill=tkinter.constants.X, expand=1) - sticky = tkinter.constants.E + tkinter.constants.W - body.grid_columnconfigure(1, weight=2) - tkinter.Label(body, text="Key file").grid(row=0) - self.keypath = tkinter.Entry(body, width=30) - self.keypath.grid(row=0, column=1, sticky=sticky) - if os.path.exists("bnepubkey.b64"): - self.keypath.insert(0, "bnepubkey.b64") - button = tkinter.Button(body, text="...", command=self.get_keypath) - button.grid(row=0, column=2) - tkinter.Label(body, text="Input file").grid(row=1) - self.inpath = tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = tkinter.Button(body, text="...", command=self.get_inpath) - button.grid(row=1, column=2) - tkinter.Label(body, text="Output file").grid(row=2) - self.outpath = tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = tkinter.Frame(self) - buttons.pack() - botton = tkinter.Button( - buttons, text="Decrypt", width=10, command=self.decrypt) - botton.pack(side=tkinter.constants.LEFT) - tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) - button = tkinter.Button( - buttons, text="Quit", width=10, command=self.quit) - button.pack(side=tkinter.constants.RIGHT) - - def get_keypath(self): - keypath = tkinter.filedialog.askopenfilename( - parent=None, title="Select Barnes & Noble \'.b64\' key file", - defaultextension=".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, tkinter.constants.END) - self.keypath.insert(0, keypath) - return - - def get_inpath(self): - inpath = tkinter.filedialog.askopenfilename( - parent=None, title="Select B&N-encrypted ePub file to decrypt", - defaultextension=".epub", filetypes=[('ePub files', '.epub')]) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, tkinter.constants.END) - self.inpath.insert(0, inpath) - return - - def get_outpath(self): - outpath = tkinter.filedialog.asksaveasfilename( - parent=None, title="Select unencrypted ePub file to produce", - defaultextension=".epub", filetypes=[('ePub files', '.epub')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, tkinter.constants.END) - self.outpath.insert(0, outpath) - return - - def decrypt(self): - keypath = self.keypath.get() - inpath = self.inpath.get() - outpath = self.outpath.get() - if not keypath or not os.path.exists(keypath): - self.status['text'] = "Specified key file does not exist" - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = "Specified input file does not exist" - return - if not outpath: - self.status['text'] = "Output file not specified" - return - if inpath == outpath: - self.status['text'] = "Must have different input and output files" - return - userkey = open(keypath,'rb').read() - self.status['text'] = "Decrypting..." - try: - decrypt_status = decryptBook(userkey, inpath, outpath) - except Exception as e: - self.status['text'] = "Error: {0}".format(e.args[0]) - return - if decrypt_status == 0: - self.status['text'] = "File successfully decrypted" - else: - self.status['text'] = "The was an error decrypting the file." - - root = tkinter.Tk() - root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(300, 0) - DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_plugin/scriptinterface.py b/DeDRM_plugin/scriptinterface.py index 25a6c09..1810237 100644 --- a/DeDRM_plugin/scriptinterface.py +++ b/DeDRM_plugin/scriptinterface.py @@ -50,8 +50,8 @@ def decryptepub(infile, outdir, rscpath): errlog += traceback.format_exc() errlog += str(e) rv = 1 - # now try with ignoble epub - elif ignobleepub.ignobleBook(zippath): + + # now try with ignoble epub # try with any keyfiles (*.b64) in the rscpath files = os.listdir(rscpath) filefilter = re.compile("\.b64$", re.IGNORECASE) @@ -62,7 +62,7 @@ def decryptepub(infile, outdir, rscpath): userkey = open(keypath,'r').read() #print userkey try: - rv = ignobleepub.decryptBook(userkey, zippath, outfile) + rv = ineptepub.decryptBook(userkey, zippath, outfile) if rv == 0: print("Decrypted B&N ePub with key file {0}".format(filename)) break @@ -121,7 +121,7 @@ def decryptpdb(infile, outdir, rscpath): rv = 1 socialpath = os.path.join(rscpath,'sdrmlist.txt') if os.path.exists(socialpath): - keydata = file(socialpath,'r').read() + keydata = open(socialpath,'r').read() keydata = keydata.rstrip(os.linesep) ar = keydata.split(',') for i in ar: @@ -148,7 +148,7 @@ def decryptk4mobi(infile, outdir, rscpath): pidnums = [] pidspath = os.path.join(rscpath,'pidlist.txt') if os.path.exists(pidspath): - pidstr = file(pidspath,'r').read() + pidstr = open(pidspath,'r').read() pidstr = pidstr.rstrip(os.linesep) pidstr = pidstr.strip() if pidstr != '': @@ -156,7 +156,7 @@ def decryptk4mobi(infile, outdir, rscpath): serialnums = [] serialnumspath = os.path.join(rscpath,'seriallist.txt') if os.path.exists(serialnumspath): - serialstr = file(serialnumspath,'r').read() + serialstr = open(serialnumspath,'r').read() serialstr = serialstr.rstrip(os.linesep) serialstr = serialstr.strip() if serialstr != '': diff --git a/DeDRM_plugin/topazextract.py b/DeDRM_plugin/topazextract.py index 98db615..55fa2ff 100644 --- a/DeDRM_plugin/topazextract.py +++ b/DeDRM_plugin/topazextract.py @@ -332,7 +332,7 @@ class TopazBook: keydata = self.getBookPayloadRecord(b'dkey', 0) except DrmException as e: print("no dkey record found, book may not be encrypted") - print("attempting to extrct files without a book key") + print("attempting to extract files without a book key") self.createBookDirectory() self.extractFiles() print("Successfully Extracted Topaz contents") @@ -364,7 +364,7 @@ class TopazBook: break if not bookKey: - raise DrmException("No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst))) + raise DrmException("No key found in {0:d} keys tried. Read the FAQs at noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst))) self.setBookKey(bookKey) self.createBookDirectory() |