Ton of PDF DeDRM updates

- 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
This commit is contained in:
NoDRM 2021-12-27 10:45:12 +01:00
parent 23a454205a
commit fbe9b5ea89
6 changed files with 513 additions and 77 deletions

View File

@ -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 plugins 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 plugins 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 thats what you truly mean to do. Once gone, its 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>

View File

@ -609,23 +609,14 @@ class DeDRM(FileTypePlugin):
# No DRM? # No DRM?
return self.postProcessEPUB(inf.name) 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.prefs as prefs
import calibre_plugins.dedrm.ineptpdf as ineptpdf import calibre_plugins.dedrm.ineptpdf as ineptpdf
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
dedrmprefs = prefs.DeDRM_Prefs() 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 book_uuid = None
try: try:
# Try to figure out which Adobe account this book is licensed for. # Try to figure out which Adobe account this book is licensed for.
@ -633,12 +624,8 @@ class DeDRM(FileTypePlugin):
except: except:
pass pass
if book_uuid is None: if book_uuid is not None:
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) 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))
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:
# Check if we have a key for that UUID # Check if we have a key for that UUID
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
if not book_uuid.lower() in keyname.lower(): 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)) 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. # Attempt to decrypt PDF with each encryption key (generated or provided).
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)) i = -1
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)) 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): def KindleMobiDecrypt(self,path_to_ebook):
@ -815,7 +881,7 @@ class DeDRM(FileTypePlugin):
# extracted to the appropriate places beforehand these routines # extracted to the appropriate places beforehand these routines
# look for them. # look for them.
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.k4mobidedrm import calibre_plugins.dedrm.k4mobidedrm as k4mobidedrm
dedrmprefs = prefs.DeDRM_Prefs() dedrmprefs = prefs.DeDRM_Prefs()
pids = dedrmprefs['pids'] pids = dedrmprefs['pids']
@ -883,7 +949,7 @@ class DeDRM(FileTypePlugin):
def eReaderDecrypt(self,path_to_ebook): def eReaderDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.erdr2pml import calibre_plugins.dedrm.erdr2pml as erdr2pml
dedrmprefs = prefs.DeDRM_Prefs() dedrmprefs = prefs.DeDRM_Prefs()
# Attempt to decrypt epub with each encryption key (generated or provided). # 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) decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
pass pass
elif booktype == 'pdf': elif booktype == 'pdf':
# Adobe Adept PDF (hopefully) # Adobe PDF (hopefully)
decrypted_ebook = self.PDFDecrypt(path_to_ebook) decrypted_ebook = self.PDFDecrypt(path_to_ebook)
pass pass
elif booktype == 'epub': elif booktype == 'epub':

View File

@ -90,6 +90,7 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['deobfuscate_fonts'] = self.dedrmprefs['deobfuscate_fonts'] self.tempdedrmprefs['deobfuscate_fonts'] = self.dedrmprefs['deobfuscate_fonts']
self.tempdedrmprefs['remove_watermarks'] = self.dedrmprefs['remove_watermarks'] self.tempdedrmprefs['remove_watermarks'] = self.dedrmprefs['remove_watermarks']
self.tempdedrmprefs['lcp_passphrases'] = list(self.dedrmprefs['lcp_passphrases']) 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 # Start Qt Gui dialog layout
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@ -122,7 +123,7 @@ class ConfigWidget(QWidget):
self.kindle_android_button.clicked.connect(self.kindle_android) self.kindle_android_button.clicked.connect(self.kindle_android)
self.kindle_serial_button = QtGui.QPushButton(self) 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.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_serial_button.clicked.connect(self.kindle_serials)
self.kindle_key_button = QtGui.QPushButton(self) self.kindle_key_button = QtGui.QPushButton(self)
self.kindle_key_button.setToolTip(_("Click to manage keys for Kindle for Mac/PC ebooks")) 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.setToolTip(_("Click to manage passphrases for Readium LCP ebooks"))
self.lcp_button.setText("Readium LCP ebooks") self.lcp_button.setText("Readium LCP ebooks")
self.lcp_button.clicked.connect(self.readium_lcp_keys) 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_serial_button)
button_layout.addWidget(self.kindle_android_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.bandn_button)
button_layout.addWidget(self.pdf_keys_button)
button_layout.addSpacing(15)
button_layout.addWidget(self.mobi_button) button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_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) button_layout.addWidget(self.lcp_button)
self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts")) self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts"))
self.chkFontObfuscation.setToolTip("Deobfuscates fonts in EPUB files after DRM removal") 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 = ManageKeysDialog(self,"Readium LCP passphrase",self.tempdedrmprefs['lcp_passphrases'], AddLCPKeyDialog)
d.exec_() 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 help_link_activated(self, url):
def get_help_file_resource(): def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the # 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('deobfuscate_fonts', self.chkFontObfuscation.isChecked())
self.dedrmprefs.set('remove_watermarks', self.chkRemoveWatermarks.isChecked()) self.dedrmprefs.set('remove_watermarks', self.chkRemoveWatermarks.isChecked())
self.dedrmprefs.set('lcp_passphrases', self.tempdedrmprefs['lcp_passphrases']) self.dedrmprefs.set('lcp_passphrases', self.tempdedrmprefs['lcp_passphrases'])
self.dedrmprefs.set('adobe_pdf_passphrases', self.tempdedrmprefs['adobe_pdf_passphrases'])
self.dedrmprefs.writeprefs() self.dedrmprefs.writeprefs()
def load_resource(self, name): def load_resource(self, name):
@ -1480,3 +1495,44 @@ class AddLCPKeyDialog(QDialog):
errmsg = "Please enter your LCP passphrase or click Cancel in the dialog." 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) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self) 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)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ignoblekey.py # ignoblekeyNookStudy.py
# Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al. # Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf # Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf

View File

@ -3,6 +3,7 @@
# ineptpdf.py # ineptpdf.py
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. # 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 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
@ -46,13 +47,14 @@
# 8.0.5 - Do not process DRM-free documents # 8.0.5 - Do not process DRM-free documents
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog # 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.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. Decrypts Adobe ADEPT-encrypted PDF files.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "9.0.0" __version__ = "9.1.0"
import codecs import codecs
import sys import sys
@ -131,6 +133,9 @@ def unicode_argv():
class ADEPTError(Exception): class ADEPTError(Exception):
pass pass
class ADEPTInvalidPasswordError(Exception):
pass
class ADEPTNewVersionError(Exception): class ADEPTNewVersionError(Exception):
pass 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_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_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_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]) 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): class AES(object):
MODE_CBC = 0 MODE_CBC = 0
@classmethod @classmethod
def new(cls, userkey, mode, iv): def new(cls, userkey, mode, iv, decrypt=True):
self = AES() self = AES()
self._blocksize = len(userkey) self._blocksize = len(userkey)
# mode is ignored since CBCMODE is only thing supported/used so far # mode is ignored since CBCMODE is only thing supported/used so far
@ -246,7 +252,11 @@ def _load_crypto_libcrypto():
return return
keyctx = self._keyctx = AES_KEY() keyctx = self._keyctx = AES_KEY()
self._iv = iv 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: if rv < 0:
raise ADEPTError('Failed to initialize AES key') raise ADEPTError('Failed to initialize AES key')
return self return self
@ -255,12 +265,23 @@ def _load_crypto_libcrypto():
self._keyctx = None self._keyctx = None
self._iv = 0 self._iv = 0
self._mode = 0 self._mode = 0
self._isDecrypt = None
def decrypt(self, data): def decrypt(self, data):
if not self._isDecrypt:
raise ADEPTError("AES not ready for decryption")
out = create_string_buffer(len(data)) out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0)
if rv == 0: if rv == 0:
raise ADEPTError('AES decryption failed') raise ADEPTError('AES decryption failed')
return out.raw 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) return (ARC4, RSA, AES)
@ -373,14 +394,23 @@ def _load_crypto_pycrypto():
class AES(object): class AES(object):
MODE_CBC = _AES.MODE_CBC MODE_CBC = _AES.MODE_CBC
@classmethod @classmethod
def new(cls, userkey, mode, iv): def new(cls, userkey, mode, iv, decrypt=True):
self = AES() self = AES()
self._aes = _AES.new(userkey, mode, iv) self._aes = _AES.new(userkey, mode, iv)
self._decrypt = decrypt
return self return self
def __init__(self): def __init__(self):
self._aes = None self._aes = None
self._decrypt = None
def decrypt(self, data): def decrypt(self, data):
if not self._decrypt:
raise ADEPTError("AES not ready for decrypt.")
return self._aes.decrypt(data) 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): class RSA(object):
def __init__(self, der): def __init__(self, der):
@ -422,7 +452,7 @@ ARC4, RSA, AES = _load_crypto()
# 1 = only if present in input # 1 = only if present in input
# 2 = always # 2 = always
GEN_XREF_STM = 1 GEN_XREF_STM = 0
# This is the value for the current document # This is the value for the current document
gen_xref_stm = False # will be set in PDFSerializer gen_xref_stm = False # will be set in PDFSerializer
@ -1507,6 +1537,16 @@ class PDFDocument(object):
raise PDFEncryptionError('Unknown filter: param=%r' % param) 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): def initialize_adobe_ps(self, password, docid, param):
global KEYFILEPATH global KEYFILEPATH
self.decrypt_key = self.genkey_adobe_ps(param) 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..' \ PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
# experimental aes pw support # 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)) 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) length = int_value(param.get('Length', 40)) # Key length (bits)
O = str_value(param['O']) O = str_value(param['O'])
R = int_value(param['R']) # Revision R = int_value(param['R']) # Revision
if 5 <= R:
raise PDFEncryptionError('Unknown revision: %r' % R)
U = str_value(param['U']) U = str_value(param['U'])
P = int_value(param['P']) 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 # Algorithm 3.2
password = (password+self.PASSWORD_PADDING)[:32] # 1 password = (password+self.PASSWORD_PADDING)[:32] # 1
hash = hashlib.md5(password) # 2 hash = hashlib.md5(password) # 2
@ -1580,9 +1768,13 @@ class PDFDocument(object):
hash.update(struct.pack('<l', P)) # 4 hash.update(struct.pack('<l', P)) # 4
hash.update(docid[0]) # 5 hash.update(docid[0]) # 5
# aes special handling if metadata isn't encrypted # 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')) hash.update(codecs.decode(b'ffffffff','hex'))
if 5 <= R: if R >= 3:
# 8 # 8
for _ in range(50): for _ in range(50):
hash = hashlib.md5(hash.digest()[:length//8]) hash = hashlib.md5(hash.digest()[:length//8])
@ -1603,25 +1795,100 @@ class PDFDocument(object):
is_authenticated = (u1 == U) is_authenticated = (u1 == U)
else: else:
is_authenticated = (u1[:16] == U[:16]) is_authenticated = (u1[:16] == U[:16])
if not is_authenticated:
raise ADEPTError('Password is not correct.') if is_authenticated:
self.decrypt_key = key 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 # genkey method
if V == 1 or V == 2: if V == 1 or V == 2 or V == 4:
self.genkey = self.genkey_v2 self.genkey = self.genkey_v2
elif V == 3: elif V == 3:
self.genkey = self.genkey_v3 self.genkey = self.genkey_v3
elif V == 4: elif V >= 5:
self.genkey = self.genkey_v2 self.genkey = self.genkey_v5
#self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
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 # rc4
if V != 4: if V < 4:
self.decipher = self.decipher_rc4 # XXX may be AES self.decipher = self.decrypt_rc4 # XXX may be AES
# aes # aes
elif V == 4 and length == 128: if not set_decipher:
self.decipher = self.decipher_aes # This should usually already be set by now.
elif V == 4 and length == 256: # If it's not, assume that V4 and newer are using AES
raise PDFNotImplementedError('AES256 encryption is currently unsupported') if V >= 4:
self.decipher = self.decrypt_aes
self.ready = True self.ready = True
return return
@ -1776,17 +2043,11 @@ class PDFDocument(object):
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)] key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
return key return key
def decrypt_aes(self, objid, genno, data): def genkey_v5(self, objid, genno):
key = self.genkey(objid, genno) # Looks like they stopped this useless obfuscation.
ivector = data[:16] return self.decrypt_key
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 decrypt_aes256(self, objid, genno, data): def decrypt_aes(self, objid, genno, data):
key = self.genkey(objid, genno) key = self.genkey(objid, genno)
ivector = data[:16] ivector = data[:16]
data = data[16:] data = data[16:]
@ -2330,6 +2591,17 @@ def decryptBook(userkey, inpath, outpath, inept=True):
return 0 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(): def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)

View File

@ -31,6 +31,7 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['pids'] = [] self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = [] self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['lcp_passphrases'] = [] self.dedrmprefs.defaults['lcp_passphrases'] = []
self.dedrmprefs.defaults['adobe_pdf_passphrases'] = []
self.dedrmprefs.defaults['adobewineprefix'] = "" self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = "" self.dedrmprefs.defaults['kindlewineprefix'] = ""
@ -54,6 +55,8 @@ class DeDRM_Prefs():
self.dedrmprefs['serials'] = [] self.dedrmprefs['serials'] = []
if self.dedrmprefs['lcp_passphrases'] == []: if self.dedrmprefs['lcp_passphrases'] == []:
self.dedrmprefs['lcp_passphrases'] = [] self.dedrmprefs['lcp_passphrases'] = []
if self.dedrmprefs['adobe_pdf_passphrases'] == []:
self.dedrmprefs['adobe_pdf_passphrases'] = []
def __getitem__(self,kind = None): def __getitem__(self,kind = None):
if kind is not None: if kind is not None: