diff --git a/.gitignore b/.gitignore
index 4564a12..f3f3d47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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}
Credits:
-- NoDRM for a bunch of updates and the Readium LCP support
+- NoDRM for a bunch of updates and maintenance since November 2021, and the Readium LCP support
- The Dark Reverser for the Mobipocket and eReader scripts
- i♥cabbages for the Adobe Digital Editions scripts
- Skindle aka Bart Simpson for the Amazon Kindle for PC script
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
similarity index 98%
rename from DeDRM_plugin/activitybar.py
rename to 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
similarity index 97%
rename from DeDRM_plugin/scrolltextwidget.py
rename to 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
-#
-
-#
-# 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} ".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()