+ file = ZipFile(open(inpath, 'rb'))
+
+ license = json.loads(file.read('META-INF/license.lcpl'))
+ print("LCP: Found LCP-encrypted book {0}".format(license["id"]))
+
+ user_info_string1 = returnUserInfoStringForLicense(license, None)
+ if (user_info_string1 is not None):
+ print("LCP: Account information: " + user_info_string1)
+
+ # Check algorithm:
+ if license["encryption"]["profile"] == "http://readium.org/lcp/basic-profile":
+ print("LCP: Book is using lcp/basic-profile encryption.")
+ transform_algo = LCPTransform.secret_transform_basic
+ elif license["encryption"]["profile"] == "http://readium.org/lcp/profile-1.0":
+ print("LCP: Book is using lcp/profile-1.0 encryption")
+ transform_algo = LCPTransform.secret_transform_profile10
+ else:
+ file.close()
+ raise LCPError("Book is using an unknown LCP encryption standard: {0}".format(license["encryption"]["profile"]))
+
+ if (
+ "algorithm" in license["encryption"]["content_key"] and
+ license["encryption"]["content_key"]["algorithm"] != "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
+ ):
+ file.close()
+ raise LCPError("Book is using an unknown LCP encryption algorithm: {0}".format(license["encryption"]["content_key"]["algorithm"]))
+
+ key_check = license["encryption"]["user_key"]["key_check"]
+ encrypted_content_key = license["encryption"]["content_key"]["encrypted_value"]
+
+ # Prepare a list of encryption keys to test:
+ password_hashes = []
+
+ # Some providers hard-code the passphrase in the LCPL file. That doesn't happen often,
+ # but when it does, these files can be decrypted without knowing any passphrase.
+
+ if "value" in license["encryption"]["user_key"]:
+ try:
+ password_hashes.append(binascii.hexlify(base64.decodebytes(license["encryption"]["user_key"]["value"].encode())).decode("ascii"))
+ except AttributeError:
+ # Python 2
+ password_hashes.append(binascii.hexlify(base64.decodestring(license["encryption"]["user_key"]["value"].encode())).decode("ascii"))
+ if "hex_value" in license["encryption"]["user_key"]:
+ password_hashes.append(binascii.hexlify(bytearray.fromhex(license["encryption"]["user_key"]["hex_value"])).decode("ascii"))
+
+ # Hash all the passwords provided by the user:
+ for possible_passphrase in passphrases:
+ password_hashes.append(possible_passphrase)
+ algo = "http://www.w3.org/2001/04/xmlenc#sha256"
+ if "algorithm" in license["encryption"]["user_key"]:
+ algo = license["encryption"]["user_key"]["algorithm"]
+
+ algo, tmp_pw = LCPTransform.userpass_to_hash(possible_passphrase.encode('utf-8'), algo)
+ if tmp_pw is not None:
+ password_hashes.append(tmp_pw)
+
+ # For all the password hashes, check if one of them decrypts the book:
+ correct_password_hash = None
+
+ for possible_hash in password_hashes:
+ transformed_hash = possible_hash
+ print("trying {0}".format(transformed_hash))
+ try:
+ decrypted = None
+ decrypted = dataDecryptLCP(key_check, transformed_hash)
+ except:
+ pass
+
+ if (decrypted is not None and decrypted.decode("ascii", errors="ignore") == license["id"]):
+ # Found correct password hash, hooray!
+ correct_password_hash = transformed_hash
+ break
+
+ transformed_hash = transform_algo(possible_hash)
+ print("trying {0}".format(transformed_hash))
+ try:
+ decrypted = None
+ decrypted = dataDecryptLCP(key_check, transformed_hash)
+ except:
+ pass
+
+ if (decrypted is not None and decrypted.decode("ascii", errors="ignore") == license["id"]):
+ # Found correct password hash, hooray!
+ correct_password_hash = transformed_hash
+ break
+
+
+ # Print an error message if none of the passwords worked
+ if (correct_password_hash is None):
+ print("LCP: Tried {0} passphrases, but none of them could decrypt the book ...".format(len(password_hashes) / 2))
+
+ # Print password hint, if available
+ if ("text_hint" in license["encryption"]["user_key"] and license["encryption"]["user_key"]["text_hint"] != ""):
+ print("LCP: The book distributor has given you the following passphrase hint: \"{0}\"".format(license["encryption"]["user_key"]["text_hint"]))
+
+ print("LCP: Enter the correct passphrase in the DeDRM plugin settings, then try again.")
+
+ # Print password reset instructions, if available
+ for link in license["links"]:
+ if ("rel" in link and link["rel"] == "hint"):
+ print("LCP: You may be able to find or reset your LCP passphrase on the following webpage: {0}".format(link["href"]))
+ break
+
+
+ file.close()
+ raise LCPError("No correct passphrase found")
+
+ print("LCP: Found correct passphrase, decrypting book ...")
+ user_info_string2 = returnUserInfoStringForLicense(license, correct_password_hash)
+ if (user_info_string2 is not None):
+ if (user_info_string1 != user_info_string2):
+ print("LCP: Account information: " + user_info_string2)
+
+
+ # Take the key we found and decrypt the content key:
+ decrypted_content_key = dataDecryptLCP(encrypted_content_key, correct_password_hash)
+
+ if decrypted_content_key is None:
+ raise LCPError("Decrypted content key is None")
+
+ # Begin decrypting
+
+ if 'META-INF/encryption.xml' in file.namelist():
+ encryption = file.read('META-INF/encryption.xml')
+ else:
+ encryption = None
+ decryptor = Decryptor(decrypted_content_key, encryption)
+ kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
+ for link in license.get("links", []):
+ if link.get("rel") == "publication":
+ content_type = link.get("type")
+ break
+
+ if content_type in ["application/pdf+lcp", "application/pdf"]:
+ # Check how many PDF files there are.
+ # Usually, an LCP-protected PDF/ZIP is only supposed to contain one
+ # PDF file, but if there are multiple, return a ZIP that contains them all.
+
+ pdf_files = []
+ for filename in file.namelist():
+ if filename.endswith(".pdf"):
+ pdf_files.append(filename)
+
+ if len(pdf_files) == 0:
+ file.close()
+ raise LCPError("Error: Book is an LCP-protected PDF, but doesn't contain any PDF files ...")
+
+ elif len(pdf_files) == 1:
+ # One PDF file found - extract and return that.
+ pdfdata = file.read(pdf_files[0])
+ outputname = parent_object.temporary_file(".pdf").name
+ print("LCP: Successfully decrypted, exporting to {0}".format(outputname))
+
+ with open(outputname, 'wb') as f:
+ f.write(decryptor.decrypt(pdf_files[0], pdfdata))
+
+ file.close()
+ return outputname
+
+ else:
+ # Multiple PDFs found
+ outputname = parent_object.temporary_file(".zip").name
+ with closing(ZipFile(open(outputname, 'wb'), 'w', **kwds)) as outfile:
+ for path in pdf_files:
+ data = file.read(path)
+ outfile.writestr(path, decryptor.decrypt(path, data))
+
+ print("LCP: Successfully decrypted a multi-PDF ZIP file, exporting to {0}".format(outputname))
+ file.close()
+ return outputname
+
+ else:
+ # Not a PDF -> EPUB
+
+ if content_type == "application/epub+zip":
+ outputname = parent_object.temporary_file(".epub").name
+ else:
+ outputname = parent_object.temporary_file(".zip").name
+
+ with closing(ZipFile(open(outputname, 'wb'), 'w', **kwds)) as outfile:
+
+ # mimetype must be 1st file. Remove from list and manually add at the beginning
+ namelist = file.namelist()
+ namelist.remove("mimetype")
+ namelist.remove("META-INF/license.lcpl")
+
+ for path in (["mimetype"] + namelist):
+ data = file.read(path)
+ zi = ZipInfo(path)
+
+ if path == "META-INF/encryption.xml":
+ # Check if that's still needed
+ if (decryptor.check_if_remaining()):
+ data = decryptor.get_xml()
+ print("LCP: Adding encryption.xml for the remaining files.")
+ else:
+ continue
+
+ try:
+ oldzi = file.getinfo(path)
+ if path == "mimetype":
+ zi.compress_type = ZIP_STORED
+ else:
+ zi.compress_type = ZIP_DEFLATED
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ zi.external_attr = oldzi.external_attr
+ zi.create_system = oldzi.create_system
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
+ except:
+ pass