]> xmof Git - DeDRM.git/commitdiff
Ton of PDF DeDRM updates
authorNoDRM <no_drm123@protonmail.com>
Mon, 27 Dec 2021 09:45:12 +0000 (10:45 +0100)
committerNoDRM <no_drm123@protonmail.com>
Mon, 27 Dec 2021 09:45:12 +0000 (10:45 +0100)
- Support "Standard" and "Adobe.APS" encryptions
- Support decrypting with owner password instead of user password
- New function to return encryption filter name
- Support for V=5, R=5 and R=6 PDF files
- Support for AES256-encrypted PDF files
- Disable broken cross-reference streams in output

DeDRM_plugin/DeDRM_PDF passphrase_Help.htm [new file with mode: 0644]
DeDRM_plugin/__init__.py
DeDRM_plugin/config.py
DeDRM_plugin/ignoblekeyNookStudy.py
DeDRM_plugin/ineptpdf.py
DeDRM_plugin/prefs.py

diff --git a/DeDRM_plugin/DeDRM_PDF passphrase_Help.htm b/DeDRM_plugin/DeDRM_PDF passphrase_Help.htm
new file mode 100644 (file)
index 0000000..77337e4
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+       "http://www.w3.org/TR/html4/strict.dtd">
+
+<html>
+
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8">
+<title>Managing PDF passwords</title>
+<style type="text/css">
+span.version {font-size: 50%}
+span.bold {font-weight: bold}
+h3 {margin-bottom: 0}
+p {margin-top: 0}
+li {margin-top: 0.5em}
+</style>
+</head>
+
+<body>
+
+<h1>Managing PDF passwords</h1>
+
+<p>PDF files can be protected with a password / passphrase that will be required to open the PDF file. Enter your passphrases in the plugin settings to have the plugin automatically remove this encryption / restriction from PDF files you import. </p>
+
+
+<h3>Entering a passphrase:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new passphrase.</p>
+
+<p>Just enter your passphrase for the PDF file, then click the OK button to save the passphrase. </p>
+
+<h3>Deleting a passphrase:</h3>
+
+<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
+
+<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
+
+</body>
+
+</html>
index 0afa7125409d17b387b581e7a269b9321753d276..1571931c9ac8823dae24ee95bff7b4951021ed29 100644 (file)
@@ -609,23 +609,14 @@ class DeDRM(FileTypePlugin):
 
         # No DRM?
         return self.postProcessEPUB(inf.name)
+
+    
+    def PDFIneptDecrypt(self, path_to_ebook):
+        # Sub function to prevent PDFDecrypt from becoming too large ...
         import calibre_plugins.dedrm.prefs as prefs
         import calibre_plugins.dedrm.ineptpdf as ineptpdf
-        import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
         dedrmprefs = prefs.DeDRM_Prefs()
 
-        if (lcpdedrm.isLCPbook(path_to_ebook)):
-            try: 
-                retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
-            except:
-                print("Looks like that didn't work:")
-                raise
-
-            return retval
-        
-
-        # Not an LCP book, do the normal Adobe handling.
-
         book_uuid = None
         try: 
             # Try to figure out which Adobe account this book is licensed for.
@@ -633,12 +624,8 @@ class DeDRM(FileTypePlugin):
         except:
             pass
 
-        if book_uuid is None: 
-            print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
-        else:
-            print("{0} v{1}: {2} is a PDF ebook for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
-
-        if book_uuid is not None:
+        if book_uuid is not None: 
+            print("{0} v{1}: {2} is a PDF ebook (EBX) for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
             # Check if we have a key for that UUID
             for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
                 if not book_uuid.lower() in keyname.lower():
@@ -800,10 +787,89 @@ class DeDRM(FileTypePlugin):
 
             print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
 
+    def PDFStandardDecrypt(self, path_to_ebook):
+        # Sub function to prevent PDFDecrypt from becoming too large ...
+        import calibre_plugins.dedrm.prefs as prefs
+        import calibre_plugins.dedrm.ineptpdf as ineptpdf
+        dedrmprefs = prefs.DeDRM_Prefs()
 
-        # Something went wrong with decryption.
-        print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
-        raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+        # Attempt to decrypt PDF with each encryption key (generated or provided).  
+        i = -1
+        for userpassword in [""] + dedrmprefs['adobe_pdf_passphrases']:
+            # Try the empty password, too.
+            i = i + 1
+            userpassword = bytearray(userpassword, "utf-8")
+            if i == 0:
+                print("{0} v{1}: Trying empty password ... ".format(PLUGIN_NAME, PLUGIN_VERSION), end="")
+            else:
+                print("{0} v{1}: Trying password {2} ... ".format(PLUGIN_NAME, PLUGIN_VERSION, i), end="")
+            of = self.temporary_file(".pdf")
+
+            # Give the user password, ebook and TemporaryPersistent file to the decryption function.
+            msg = False
+            try:
+                result = ineptpdf.decryptBook(userpassword, path_to_ebook, of.name)
+                print("done")
+                msg = True
+            except ineptpdf.ADEPTInvalidPasswordError:
+                print("invalid password".format(PLUGIN_NAME, PLUGIN_VERSION))
+                msg = True
+                result = 1
+            except:
+                print("exception\n{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+                msg = True
+                traceback.print_exc()
+                result = 1
+            if not msg:
+                print("error\n{0} v{1}: Failed to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
+
+            of.close()
+
+            if  result == 0:
+                # Decryption was successful.
+                # Return the modified PersistentTemporary file to calibre.
+                print("{0} v{1}: Successfully decrypted with password {3} after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime, i))
+                return of.name
+        
+        print("{0} v{1}: Didn't manage to decrypt PDF. Make sure the correct password is entered in the settings.".format(PLUGIN_NAME, PLUGIN_VERSION))
+
+        
+    
+    def PDFDecrypt(self,path_to_ebook):
+        import calibre_plugins.dedrm.prefs as prefs
+        import calibre_plugins.dedrm.ineptpdf as ineptpdf
+        import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
+        dedrmprefs = prefs.DeDRM_Prefs()
+
+        if (lcpdedrm.isLCPbook(path_to_ebook)):
+            try: 
+                retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
+            except:
+                print("Looks like that didn't work:")
+                raise
+
+            return retval
+        
+        # Not an LCP book, do the normal Adobe handling.
+
+        pdf_encryption = ineptpdf.getPDFencryptionType(path_to_ebook)
+        if pdf_encryption is None:
+            print("{0} v{1}: {2} is an unencrypted PDF file - returning as is.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
+            return path_to_ebook
+
+        print("{0} v{1}: {2} is a PDF ebook with encryption {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), pdf_encryption))
+
+        if pdf_encryption == "EBX_HANDLER":
+            # Adobe eBook / ADEPT (normal or B&N)
+            return self.PDFIneptDecrypt(path_to_ebook)
+        elif pdf_encryption == "Standard" or pdf_encryption == "Adobe.APS":
+            return self.PDFStandardDecrypt(path_to_ebook)
+        elif pdf_encryption == "FOPN_fLock" or pdf_encryption == "FOPN_foweb":
+            print("{0} v{1}: FileOpen encryption '{2}' is unsupported.".format(PLUGIN_NAME, PLUGIN_VERSION, pdf_encryption))
+            print("{0} v{1}: Try the standalone script from the 'Tetrachroma_FileOpen_ineptpdf' folder in the Github repo.".format(PLUGIN_NAME, PLUGIN_VERSION))
+        else:
+            print("{0} v{1}: Encryption '{2}' is unsupported.".format(PLUGIN_NAME, PLUGIN_VERSION, pdf_encryption))
+            return path_to_ebook
 
 
     def KindleMobiDecrypt(self,path_to_ebook):
@@ -815,7 +881,7 @@ class DeDRM(FileTypePlugin):
         # extracted to the appropriate places beforehand these routines
         # look for them.
         import calibre_plugins.dedrm.prefs as prefs
-        import calibre_plugins.dedrm.k4mobidedrm
+        import calibre_plugins.dedrm.k4mobidedrm as k4mobidedrm
 
         dedrmprefs = prefs.DeDRM_Prefs()
         pids = dedrmprefs['pids']
@@ -883,7 +949,7 @@ class DeDRM(FileTypePlugin):
     def eReaderDecrypt(self,path_to_ebook):
 
         import calibre_plugins.dedrm.prefs as prefs
-        import calibre_plugins.dedrm.erdr2pml
+        import calibre_plugins.dedrm.erdr2pml as erdr2pml
 
         dedrmprefs = prefs.DeDRM_Prefs()
         # Attempt to decrypt epub with each encryption key (generated or provided).
@@ -927,7 +993,7 @@ class DeDRM(FileTypePlugin):
             decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
             pass
         elif booktype == 'pdf':
-            # Adobe Adept PDF (hopefully)
+            # Adobe PDF (hopefully)
             decrypted_ebook = self.PDFDecrypt(path_to_ebook)
             pass
         elif booktype == 'epub':
index a09110f4a8bbc6b32d4e898d593540b2d7e08d4f..1c5568420d8b817e9fefd1caa09feed5d7675906 100755 (executable)
@@ -90,6 +90,7 @@ class ConfigWidget(QWidget):
         self.tempdedrmprefs['deobfuscate_fonts'] = self.dedrmprefs['deobfuscate_fonts']
         self.tempdedrmprefs['remove_watermarks'] = self.dedrmprefs['remove_watermarks']
         self.tempdedrmprefs['lcp_passphrases'] = list(self.dedrmprefs['lcp_passphrases'])
+        self.tempdedrmprefs['adobe_pdf_passphrases'] = list(self.dedrmprefs['adobe_pdf_passphrases'])
 
         # Start Qt Gui dialog layout
         layout = QVBoxLayout(self)
@@ -122,7 +123,7 @@ class ConfigWidget(QWidget):
         self.kindle_android_button.clicked.connect(self.kindle_android)
         self.kindle_serial_button = QtGui.QPushButton(self)
         self.kindle_serial_button.setToolTip(_("Click to manage eInk Kindle serial numbers for Kindle ebooks"))
-        self.kindle_serial_button.setText("eInk Kindle ebooks")
+        self.kindle_serial_button.setText("Kindle eInk ebooks")
         self.kindle_serial_button.clicked.connect(self.kindle_serials)
         self.kindle_key_button = QtGui.QPushButton(self)
         self.kindle_key_button.setToolTip(_("Click to manage keys for Kindle for Mac/PC ebooks"))
@@ -144,14 +145,23 @@ class ConfigWidget(QWidget):
         self.lcp_button.setToolTip(_("Click to manage passphrases for Readium LCP ebooks"))
         self.lcp_button.setText("Readium LCP ebooks")
         self.lcp_button.clicked.connect(self.readium_lcp_keys)
+        self.pdf_keys_button = QtGui.QPushButton(self)
+        self.pdf_keys_button.setToolTip(_("Click to manage PDF file passphrases"))
+        self.pdf_keys_button.setText("Adobe PDF passwords")
+        self.pdf_keys_button.clicked.connect(self.pdf_passphrases)
+
         button_layout.addWidget(self.kindle_serial_button)
         button_layout.addWidget(self.kindle_android_button)
+        button_layout.addWidget(self.kindle_key_button)
+        button_layout.addSpacing(15)
+        button_layout.addWidget(self.adept_button)
         button_layout.addWidget(self.bandn_button)
+        button_layout.addWidget(self.pdf_keys_button)
+        button_layout.addSpacing(15)
         button_layout.addWidget(self.mobi_button)
         button_layout.addWidget(self.ereader_button)
-        button_layout.addWidget(self.adept_button)
-        button_layout.addWidget(self.kindle_key_button)
         button_layout.addWidget(self.lcp_button)
+        
 
         self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts"))
         self.chkFontObfuscation.setToolTip("Deobfuscates fonts in EPUB files after DRM removal")
@@ -207,6 +217,10 @@ class ConfigWidget(QWidget):
         d = ManageKeysDialog(self,"Readium LCP passphrase",self.tempdedrmprefs['lcp_passphrases'], AddLCPKeyDialog)
         d.exec_()
 
+    def pdf_passphrases(self):
+        d = ManageKeysDialog(self,"PDF passphrase",self.tempdedrmprefs['adobe_pdf_passphrases'], AddPDFPassDialog)
+        d.exec_()
+
     def help_link_activated(self, url):
         def get_help_file_resource():
             # Copy the HTML helpfile to the plugin directory each time the
@@ -232,6 +246,7 @@ class ConfigWidget(QWidget):
         self.dedrmprefs.set('deobfuscate_fonts', self.chkFontObfuscation.isChecked())
         self.dedrmprefs.set('remove_watermarks', self.chkRemoveWatermarks.isChecked())
         self.dedrmprefs.set('lcp_passphrases', self.tempdedrmprefs['lcp_passphrases'])
+        self.dedrmprefs.set('adobe_pdf_passphrases', self.tempdedrmprefs['adobe_pdf_passphrases'])
         self.dedrmprefs.writeprefs()
 
     def load_resource(self, name):
@@ -1480,3 +1495,44 @@ class AddLCPKeyDialog(QDialog):
             errmsg = "Please enter your LCP passphrase or click Cancel in the dialog."
             return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
         QDialog.accept(self)
+
+class AddPDFPassDialog(QDialog):
+    def __init__(self, parent=None,):
+        QDialog.__init__(self, parent)
+        self.parent = parent
+        self.setWindowTitle("{0} {1}: Add new PDF passphrase".format(PLUGIN_NAME, PLUGIN_VERSION))
+        layout = QVBoxLayout(self)
+        self.setLayout(layout)
+
+        data_group_box = QGroupBox("", self)
+        layout.addWidget(data_group_box)
+        data_group_box_layout = QVBoxLayout()
+        data_group_box.setLayout(data_group_box_layout)
+
+        key_group = QHBoxLayout()
+        data_group_box_layout.addLayout(key_group)
+        key_group.addWidget(QLabel("PDF password:", self))
+        self.key_ledit = QLineEdit("", self)
+        self.key_ledit.setToolTip("Enter the PDF file password.")
+        key_group.addWidget(self.key_ledit)
+
+        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+        self.button_box.accepted.connect(self.accept)
+        self.button_box.rejected.connect(self.reject)
+        layout.addWidget(self.button_box)
+
+        self.resize(self.sizeHint())
+
+    @property
+    def key_name(self):
+        return None
+
+    @property
+    def key_value(self):
+        return str(self.key_ledit.text())
+
+    def accept(self):
+        if len(self.key_value) == 0 or self.key_value.isspace():
+            errmsg = "Please enter a PDF password or click Cancel in the dialog."
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+        QDialog.accept(self)
index ea9785a1e837789457b961393d7c6cea1639d2c8..4152093d2d3cb26e53ccfcc8c38af1b804deee79 100644 (file)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
-# ignoblekey.py
+# ignoblekeyNookStudy.py
 # Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
 
 # Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
index 51ca17e39b3566aab767808d204ce46a1d698244..9a100963ba3d1e97d627cb262789217b38e64083 100755 (executable)
@@ -3,6 +3,7 @@
 
 # ineptpdf.py
 # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
+# Copyright © 2021 by noDRM
 
 # Released under the terms of the GNU General Public Licence, version 3
 # <http://www.gnu.org/licenses/>
 #   8.0.5 - Do not process DRM-free documents
 #   8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
 #   9.0.0 - Add Python 3 compatibility for calibre 5
+#   9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
 
 """
 Decrypts Adobe ADEPT-encrypted PDF files.
 """
 
 __license__ = 'GPL v3'
-__version__ = "9.0.0"
+__version__ = "9.1.0"
 
 import codecs
 import sys
@@ -131,6 +133,9 @@ def unicode_argv():
 class ADEPTError(Exception):
     pass
 
+class ADEPTInvalidPasswordError(Exception):
+    pass
+
 class ADEPTNewVersionError(Exception):
     pass
 
@@ -184,6 +189,7 @@ def _load_crypto_libcrypto():
 
     AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
     AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
+    AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',[c_char_p, c_int, AES_KEY_p])
 
     RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p])
     RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p])
@@ -236,7 +242,7 @@ def _load_crypto_libcrypto():
     class AES(object):
         MODE_CBC = 0
         @classmethod
-        def new(cls, userkey, mode, iv):
+        def new(cls, userkey, mode, iv, decrypt=True):
             self = AES()
             self._blocksize = len(userkey)
             # mode is ignored since CBCMODE is only thing supported/used so far
@@ -246,7 +252,11 @@ def _load_crypto_libcrypto():
                 return
             keyctx = self._keyctx = AES_KEY()
             self._iv = iv
-            rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
+            self._isDecrypt = decrypt
+            if decrypt: 
+                rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
+            else:
+                rv = AES_set_encrypt_key(userkey, len(userkey) * 8, keyctx)
             if rv < 0:
                 raise ADEPTError('Failed to initialize AES key')
             return self
@@ -255,12 +265,23 @@ def _load_crypto_libcrypto():
             self._keyctx = None
             self._iv = 0
             self._mode = 0
+            self._isDecrypt = None
         def decrypt(self, data):
+            if not self._isDecrypt:
+                raise ADEPTError("AES not ready for decryption")
             out = create_string_buffer(len(data))
             rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0)
             if rv == 0:
                 raise ADEPTError('AES decryption failed')
             return out.raw
+        def encrypt(self, data):
+            if self._isDecrypt:
+                raise ADEPTError("AES not ready for encryption")
+            out = create_string_buffer(len(data))
+            rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 1)
+            if rv == 0:
+                raise ADEPTError('AES decryption failed')
+            return out.raw
 
     return (ARC4, RSA, AES)
 
@@ -373,14 +394,23 @@ def _load_crypto_pycrypto():
     class AES(object):
         MODE_CBC = _AES.MODE_CBC
         @classmethod
-        def new(cls, userkey, mode, iv):
+        def new(cls, userkey, mode, iv, decrypt=True):
             self = AES()
             self._aes = _AES.new(userkey, mode, iv)
+            self._decrypt = decrypt
             return self
         def __init__(self):
             self._aes = None
+            self._decrypt = None
         def decrypt(self, data):
+            if not self._decrypt:
+                raise ADEPTError("AES not ready for decrypt.")
+
             return self._aes.decrypt(data)
+        def encrypt(self, data):
+            if self._decrypt:
+                raise ADEPTError("AES not ready for encrypt.")
+            return self._aes.encrypt(data)
 
     class RSA(object):
         def __init__(self, der):
@@ -422,7 +452,7 @@ ARC4, RSA, AES = _load_crypto()
 # 1 = only if present in input
 # 2 = always
 
-GEN_XREF_STM = 1
+GEN_XREF_STM = 0
 
 # This is the value for the current document
 gen_xref_stm = False # will be set in PDFSerializer
@@ -1507,6 +1537,16 @@ class PDFDocument(object):
 
         raise PDFEncryptionError('Unknown filter: param=%r' % param)
 
+    def initialize_and_return_filter(self):
+        if not self.encryption:
+            self.is_printable = self.is_modifiable = self.is_extractable = True
+            self.ready = True
+            return None
+
+        (docid, param) = self.encryption
+        type = literal_name(param['Filter'])
+        return type
+
     def initialize_adobe_ps(self, password, docid, param):
         global KEYFILEPATH
         self.decrypt_key = self.genkey_adobe_ps(param)
@@ -1549,30 +1589,178 @@ class PDFDocument(object):
     PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
                        b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
     # experimental aes pw support
-    def initialize_standard(self, password, docid, param):
-        # copy from a global variable
+
+    def check_user_password(self, password, docid, param):
+        V = int_value(param.get('V', 0))
+        if V < 5: 
+            return self.check_user_password_V4(password, docid, param)
+        else:
+            return self.check_user_password_V5(password, param)
+
+    def check_owner_password(self, password, docid, param):
+        V = int_value(param.get('V', 0))
+        if V < 5: 
+            return self.check_owner_password_V4(password, docid, param)
+        else:
+            return self.check_owner_password_V5(password, param)
+
+    def check_user_password_V5(self, password, param):
+        U = str_value(param['U'])
+        userdata = U[:32]
+        salt = U[32:32+8]
+        # Truncate password:
+        password = password[:min(127, len(password))]
+        if self.hash_V5(password, salt, b"", param) == userdata:
+            return True
+        return None
+
+    def check_owner_password_V5(self, password, param):
+        U = str_value(param['U'])
+        O = str_value(param['O'])
+        userdata = U[:48]
+        ownerdata = O[:32]
+        salt = O[32:32+8]
+        # Truncate password:
+        password = password[:min(127, len(password))]
+        if self.hash_V5(password, salt, userdata, param) == ownerdata:
+            return True
+        return None
+
+    def recover_encryption_key_with_password(self, password, docid, param):
+        # Truncate password:
+        key_password = password[:min(127, len(password))]
+
+        if self.check_owner_password_V5(key_password, param):
+            O = str_value(param['O'])
+            U = str_value(param['U'])
+            OE = str_value(param['OE'])
+            key_salt = O[40:40+8]
+            user_data = U[:48]
+            encrypted_file_key = OE[:32]
+        elif self.check_user_password_V5(key_password, param):
+            U = str_value(param['U'])
+            UE = str_value(param['UE'])
+            key_salt = U[40:40+8]
+            user_data = b""
+            encrypted_file_key = UE[:32]
+        else:
+            raise Exception("Trying to recover key, but neither user nor owner pass is correct.")
+
+        intermediate_key = self.hash_V5(key_password, key_salt, user_data, param)
+
+        file_key = self.process_with_aes(intermediate_key, False, encrypted_file_key)
+
+        return file_key
+
+
+    def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
+        if iv is None:
+            keylen = len(key)
+            iv = bytes([0x00]*keylen)
+
+        if not encrypt:
+            plaintext = AES.new(key,AES.MODE_CBC,iv, True).decrypt(data)
+            return plaintext
+        else:
+            aes = AES.new(key, AES.MODE_CBC, iv, False)
+            new_data = bytes(data * repetitions)           
+            crypt = aes.encrypt(new_data)
+            return crypt
+
+
+    def hash_V5(self, password, salt, userdata, param):
+        R = int_value(param['R'])
+        K = SHA256(password + salt + userdata)
+        if R < 6:
+            return K
+        elif R == 6: 
+            round_number = 0
+            done = False
+            while (not done):
+                round_number = round_number + 1
+                K1 = password + K + userdata
+                if len(K1) < 32:
+                    raise Exception("K1 < 32 ...")
+                #def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
+                E = self.process_with_aes(K[:16], True, K1, 64, K[16:32])
+
+                E_mod_3 = 0
+                for i in range(16):
+                    E_mod_3 += E[i]
+                E_mod_3 = E_mod_3 % 3
+
+                if E_mod_3 == 0:
+                    ctx = hashlib.sha256()
+                    ctx.update(E)
+                    K = ctx.digest()
+                elif E_mod_3 == 1: 
+                    ctx = hashlib.sha384()
+                    ctx.update(E)
+                    K = ctx.digest()
+                else: 
+                    ctx = hashlib.sha512()
+                    ctx.update(E)
+                    K = ctx.digest()
+
+                if round_number >= 64:
+                    ch = int.from_bytes(E[-1:], "big", signed=False)
+                    if ch <= round_number - 32:
+                        done = True
+
+            result = K[0:32]
+            return result
+        else:
+            raise NotImplementedError("Revision > 6 not supported.")
+
+
+    def check_owner_password_V4(self, password, docid, param):
+
+        # compute_O_rc4_key:
+        V = int_value(param.get('V', 0))
+        if V >= 5:
+            raise Exception("compute_O_rc4_key not possible with V>= 5")
+    
+        R = int_value(param.get('R', 0))
+
+        length = int_value(param.get('Length', 40)) # Key length (bits)
+        password = (password+self.PASSWORD_PADDING)[:32]
+        hash = hashlib.md5(password)
+        if R >= 3:
+            for _ in range(50):
+                hash = hashlib.md5(hash.digest()[:length//8])
+        hash = hash.digest()[:length//8]
+        
+        # "hash" is the return value of compute_O_rc4_key
+
+        Odata = str_value(param.get('O')) 
+        # now call iterate_rc4 ...
+        x = ARC4.new(hash).decrypt(Odata) # 4
+        if R >= 3:
+            for i in range(1,19+1):
+                k = b''.join(bytes([c ^ i]) for c in hash )
+                x = ARC4.new(k).decrypt(x)
+
+        # TODO: remove the padding string from the end of the data!
+        for ct in range(1, len(x)):
+            new_x = x[:ct]
+            enc_key = self.check_user_password(new_x, docid, param)
+            if enc_key is not None:
+                return enc_key
+
+        return False
+
+        
+
+    
+    def check_user_password_V4(self, password, docid, param):
+
         V = int_value(param.get('V', 0))
-        if (V <=0 or V > 4):
-            raise PDFEncryptionError('Unknown algorithm: param=%r' % param)
         length = int_value(param.get('Length', 40)) # Key length (bits)
         O = str_value(param['O'])
         R = int_value(param['R']) # Revision
-        if 5 <= R:
-            raise PDFEncryptionError('Unknown revision: %r' % R)
         U = str_value(param['U'])
         P = int_value(param['P'])
-        try:
-            EncMetadata = str_value(param['EncryptMetadata'])
-        except:
-            EncMetadata = b'True'
-        self.is_printable = bool(P & 4)
-        self.is_modifiable = bool(P & 8)
-        self.is_extractable = bool(P & 16)
-        self.is_annotationable = bool(P & 32)
-        self.is_formsenabled = bool(P & 256)
-        self.is_textextractable = bool(P & 512)
-        self.is_assemblable = bool(P & 1024)
-        self.is_formprintable = bool(P & 2048)
+
         # Algorithm 3.2
         password = (password+self.PASSWORD_PADDING)[:32] # 1
         hash = hashlib.md5(password) # 2
@@ -1580,9 +1768,13 @@ class PDFDocument(object):
         hash.update(struct.pack('<l', P)) # 4
         hash.update(docid[0]) # 5
         # aes special handling if metadata isn't encrypted
-        if EncMetadata == ('False' or 'false'):
+        try:
+            EncMetadata = str_value(param['EncryptMetadata'])
+        except:
+            EncMetadata = b'True'
+        if (EncMetadata == ('False' or 'false') or V < 4) and R >= 4:
             hash.update(codecs.decode(b'ffffffff','hex'))
-        if 5 <= R:
+        if R >= 3:
             # 8
             for _ in range(50):
                 hash = hashlib.md5(hash.digest()[:length//8])
@@ -1603,25 +1795,100 @@ class PDFDocument(object):
             is_authenticated = (u1 == U)
         else:
             is_authenticated = (u1[:16] == U[:16])
-        if not is_authenticated:
-            raise ADEPTError('Password is not correct.')
-        self.decrypt_key = key
+        
+        if is_authenticated:
+            return key
+        
+        return None
+
+    def initialize_standard(self, password, docid, param):
+
+        self.decrypt_key = None
+
+
+        # copy from a global variable
+        V = int_value(param.get('V', 0))
+        if (V <=0 or V > 5):
+            raise PDFEncryptionError('Unknown algorithm: %r' % V)
+        R = int_value(param['R']) # Revision
+        if R >= 7:
+            raise PDFEncryptionError('Unknown revision: %r' % R)
+
+        # check owner pass:
+        retval = self.check_owner_password(password, docid, param)
+        if retval is True or retval is not None:
+            #print("Owner pass is valid - " + str(retval))
+            if retval is True:
+                self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
+            else:
+                self.decrypt_key = retval
+
+        if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
+            # That's not the owner password. Check if it's the user password.
+            retval = self.check_user_password(password, docid, param)
+            if retval is True or retval is not None:
+                #print("User pass is valid")
+                if retval is True:
+                    self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
+                else:
+                    self.decrypt_key = retval
+
+        if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
+            raise ADEPTInvalidPasswordError("Password invalid.")  
+
+
+        P = int_value(param['P'])
+
+        self.is_printable = bool(P & 4)
+        self.is_modifiable = bool(P & 8)
+        self.is_extractable = bool(P & 16)
+        self.is_annotationable = bool(P & 32)
+        self.is_formsenabled = bool(P & 256)
+        self.is_textextractable = bool(P & 512)
+        self.is_assemblable = bool(P & 1024)
+        self.is_formprintable = bool(P & 2048)
+
+
         # genkey method
-        if V == 1 or V == 2:
+        if V == 1 or V == 2 or V == 4:
             self.genkey = self.genkey_v2
         elif V == 3:
             self.genkey = self.genkey_v3
-        elif V == 4:
-            self.genkey = self.genkey_v2
-        #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
+        elif V >= 5:
+            self.genkey = self.genkey_v5
+
+        set_decipher = False
+
+        if V >= 4:
+            # Check if we need new genkey_v4 - only if we're using AES. 
+            try: 
+                for key in param['CF']:
+                    algo = str(param["CF"][key]["CFM"])
+                    if algo == "/AESV2":
+                        if V == 4:
+                            self.genkey = self.genkey_v4
+                        set_decipher = True
+                        self.decipher = self.decrypt_aes
+                    elif algo == "/AESV3":
+                        if V == 4:
+                            self.genkey = self.genkey_v4
+                        set_decipher = True
+                        self.decipher = self.decrypt_aes
+                    elif algo == "/V2":
+                        set_decipher = True
+                        self.decipher = self.decrypt_rc4
+            except:
+                pass
+
         # rc4
-        if V != 4:
-            self.decipher = self.decipher_rc4  # XXX may be AES
+        if V < 4:
+            self.decipher = self.decrypt_rc4  # XXX may be AES
         # aes
-        elif V == 4 and length == 128:
-            self.decipher = self.decipher_aes
-        elif V == 4 and length == 256:
-            raise PDFNotImplementedError('AES256 encryption is currently unsupported')
+        if not set_decipher:
+            # This should usually already be set by now. 
+            # If it's not, assume that V4 and newer are using AES
+            if V >= 4:
+                self.decipher = self.decrypt_aes
         self.ready = True
         return
 
@@ -1776,17 +2043,11 @@ class PDFDocument(object):
         key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
         return key
 
-    def decrypt_aes(self, objid, genno, data):
-        key = self.genkey(objid, genno)
-        ivector = data[:16]
-        data = data[16:]
-        plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
-        # remove pkcs#5 aes padding
-        cutter = -1 * plaintext[-1]
-        plaintext = plaintext[:cutter]
-        return plaintext
+    def genkey_v5(self, objid, genno):
+        # Looks like they stopped this useless obfuscation.
+        return self.decrypt_key
 
-    def decrypt_aes256(self, objid, genno, data):
+    def decrypt_aes(self, objid, genno, data):
         key = self.genkey(objid, genno)
         ivector = data[:16]
         data = data[16:]
@@ -2330,6 +2591,17 @@ def decryptBook(userkey, inpath, outpath, inept=True):
     return 0
 
 
+def getPDFencryptionType(inpath):
+    if RSA is None:
+        raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
+    with open(inpath, 'rb') as inf:
+        doc = doc = PDFDocument()
+        parser = PDFParser(doc, inf)
+        filter = doc.initialize_and_return_filter()
+        return filter
+
+
+
 def cli_main():
     sys.stdout=SafeUnbuffered(sys.stdout)
     sys.stderr=SafeUnbuffered(sys.stderr)
index c83e0a5eeba6ae3cbc5403fe2b22330108df3631..aee4846decad44c2bfd152fcb93770f7c8840b09 100755 (executable)
@@ -31,6 +31,7 @@ class DeDRM_Prefs():
         self.dedrmprefs.defaults['pids'] = []
         self.dedrmprefs.defaults['serials'] = []
         self.dedrmprefs.defaults['lcp_passphrases'] = []
+        self.dedrmprefs.defaults['adobe_pdf_passphrases'] = []
         self.dedrmprefs.defaults['adobewineprefix'] = ""
         self.dedrmprefs.defaults['kindlewineprefix'] = ""
 
@@ -54,6 +55,8 @@ class DeDRM_Prefs():
             self.dedrmprefs['serials'] = []
         if self.dedrmprefs['lcp_passphrases'] == []:
             self.dedrmprefs['lcp_passphrases'] = []
+        if self.dedrmprefs['adobe_pdf_passphrases'] == []:
+            self.dedrmprefs['adobe_pdf_passphrases'] = []
 
     def __getitem__(self,kind = None):
         if kind is not None: