Guest User

Untitled

a guest
Jan 11th, 2026
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.38 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3.  
  4. #Adapted from https://notabug.org/uhuxybim/DeDRM_tools-LCP
  5. #Adaption by meaclam, 2022
  6.  
  7. # lcpdedrm.py
  8. # Copyright © 2021 NoDRM
  9.  
  10. # Released under the terms of the GNU General Public Licence, version 3
  11. # <http://www.gnu.org/licenses/>
  12.  
  13. """
  14. Decrypt Readium LCP encrypted audiobooks.
  15. """
  16.  
  17. __license__ = 'GPL v3'
  18. __version__ = "1"
  19.  
  20. from genericpath import exists
  21. from itertools import count
  22. import json
  23. import hashlib
  24. import base64
  25. import binascii
  26. import os.path
  27. import shutil
  28. from zipfile import ZipFile
  29. import tkinter as tk
  30. from tkinter import filedialog
  31. from tkinter import simpledialog
  32. import pip
  33. pip.main(['install','pycryptodome'])
  34. from Crypto.Cipher import AES
  35.  
  36.  
  37. class Run:
  38.     def __init__(self):
  39.         root = tk.Tk()
  40.         root.withdraw()
  41.  
  42.         #Select a zip file with .mp3 audio files, license.lcpl and manifest.json
  43.         file_path = filedialog.askopenfilename()
  44.         self.path_to_ebook = file_path
  45.  
  46.         if exists("passphrase.txt"):
  47.             f = open("passphrase.txt", "r")
  48.             self.passphrase = [f.read()]
  49.             f.close()
  50.         else:
  51.             self.passphrase = -1
  52.         #self.passphrase = ["01485216"]   # <- uncomment and edit here
  53.  
  54.     def start(self):
  55.         if(self.passphrase != -1):
  56.             self.passphrase = [simpledialog.askstring("Passphrase", "What is your passphrase?", initialvalue=self.passphrase[0])]
  57.         else:
  58.             self.passphrase = [simpledialog.askstring("Passphrase", "What is your passphrase?")]
  59.  
  60.         f = open("passphrase.txt", "w")
  61.         f.write(self.passphrase[0])
  62.         f.close()
  63.  
  64.         decryptLCPbook(self.path_to_ebook, self.passphrase, self)
  65.  
  66.  
  67. class Decryptor(object):
  68.     def __init__(self, bookkey):
  69.         self.book_key = bookkey
  70.  
  71.     def decrypt(self, data):
  72.         aes = AES.new(self.book_key, AES.MODE_CBC, data[:16])
  73.         data = aes.decrypt(data[16:])
  74.  
  75.         return data
  76.  
  77.  
  78. class LCPError(Exception):
  79.     pass
  80.  
  81.  
  82. class LCPTransform:
  83.  
  84.     @staticmethod
  85.     def secret_transform_basic(input_hash):
  86.         # basic profile doesn't have any transformation
  87.         # Takes key input as hexdigest and outputs it as hexdigest
  88.         return input_hash
  89.  
  90.     @staticmethod
  91.     def secret_transform_profile10(input_hash):
  92.         # Takes an input sha256 hash as hexdigest and transforms that according to the profile-1.0 spec.
  93.         # This 64-byte master key is basically all that distinguishes the open source "open for everyone" version
  94.         # from the so-called "open source" closed-source-version that's actually being used by book distributors.
  95.         # 64 byte master key = 64 iterations
  96.  
  97.         # This function is what the documentation describes as "uk = userkey(h)", the "secret userkey transform"
  98.  
  99.         # 1. Take input
  100.         # 2. Hash it
  101.         # 3. Add one byte from the master key to the end of the hash
  102.         # 4. Hash that result again
  103.         # 5. Go back to 3. until you run out of bytes.
  104.         # 6. The result is the key.
  105.  
  106.         masterkey = "b3a07c4d42880e69398e05392405050efeea0664c0b638b7c986556fa9b58d77b31a40eb6a4fdba1e4537229d9f779daad1cc41ee968153cb71f27dc9696d40f"
  107.         masterkey = bytearray.fromhex(masterkey)
  108.  
  109.         current_hash = bytearray.fromhex(input_hash)
  110.  
  111.         for byte in masterkey:
  112.             current_hash.append(byte)
  113.             current_hash = bytearray(hashlib.sha256(current_hash).digest())
  114.         return binascii.hexlify(current_hash).decode("latin-1")
  115.  
  116.     @staticmethod
  117.     def userpass_to_hash(passphrase, algorithm):
  118.         # Check for the password algorithm. The Readium LCP standard only defines SHA256.
  119.         # The hashing standard documents they link to define a couple other hash algorithms, too.
  120.         # I've never seen them actually used in an LCP-encrypted file, so I didn't bother to implement them.
  121.  
  122.         if (algorithm == "http://www.w3.org/2001/04/xmlenc#sha256"):
  123.             algo = "SHA256"
  124.             user_password_hashed = hashlib.sha256(passphrase).hexdigest()
  125.             # This seems to be the only algorithm that's actually defined in the Readium standard.
  126.         else:
  127.             print("LCP: Book is using unsupported user key algorithm: {0}".format(algorithm))
  128.             return None, None
  129.  
  130.         return algo, user_password_hashed
  131.  
  132.  
  133. # This function decrypts data with the given key
  134. def dataDecryptLCP(b64data, hex_key):
  135.     try:
  136.         iv = base64.decodebytes(b64data.encode('ascii'))[:16]
  137.         cipher = base64.decodebytes(b64data.encode('ascii'))[16:]
  138.     except AttributeError:
  139.         iv = base64.decodestring(b64data.encode('ascii'))[:16]
  140.         cipher = base64.decodestring(b64data.encode('ascii'))[16:]
  141.  
  142.     aes = AES.new(binascii.unhexlify(hex_key), AES.MODE_CBC, iv)
  143.     temp = aes.decrypt(cipher)
  144.     try:
  145.         padding = temp[-1]
  146.         data_temp = temp[:-padding]
  147.     except TypeError:
  148.         padding = ord(temp[-1])
  149.         data_temp = temp[:-padding]
  150.  
  151.     return data_temp
  152.  
  153.  
  154. # Takes a file and a list of passphrases
  155. def decryptLCPbook(inpath, passphrases, parent_object):
  156.     file = ZipFile(open(inpath, 'rb'))
  157.  
  158.     license = json.loads(file.read('license.lcpl'))
  159.     print("LCP: Found LCP-encrypted book {0}".format(license["id"]))
  160.  
  161.     # Check algorithm:
  162.     if license["encryption"]["profile"] == "http://readium.org/lcp/basic-profile":
  163.         print("LCP: Book is using lcp/basic-profile encryption.")
  164.         transform_algo = LCPTransform.secret_transform_basic
  165.     elif license["encryption"]["profile"] == "http://readium.org/lcp/profile-1.0":
  166.         print("LCP: Book is using lcp/profile-1.0 encryption")
  167.         transform_algo = LCPTransform.secret_transform_profile10
  168.     else:
  169.         file.close()
  170.         raise LCPError("Book is using an unknown LCP encryption standard: {0}".format(license["encryption"]["profile"]))
  171.  
  172.     if (
  173.             "algorithm" in license["encryption"]["content_key"] and
  174.             license["encryption"]["content_key"]["algorithm"] != "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
  175.     ):
  176.         file.close()
  177.         raise LCPError("Book is using an unknown LCP encryption algorithm: {0}".format(
  178.             license["encryption"]["content_key"]["algorithm"]))
  179.  
  180.     key_check = license["encryption"]["user_key"]["key_check"]
  181.     encrypted_content_key = license["encryption"]["content_key"]["encrypted_value"]
  182.  
  183.     # Prepare a list of encryption keys to test:
  184.     password_hashes = []
  185.  
  186.     # Some providers hard-code the passphrase in the LCPL file. That doesn't happen often,
  187.     # but when it does, these files can be decrypted without knowing any passphrase.
  188.  
  189.     if "value" in license["encryption"]["user_key"]:
  190.         try:
  191.             password_hashes.append(
  192.                 binascii.hexlify(base64.decodebytes(license["encryption"]["user_key"]["value"].encode())).decode(
  193.                     "ascii"))
  194.         except AttributeError:
  195.             # Python 2
  196.             password_hashes.append(
  197.                 binascii.hexlify(base64.decodestring(license["encryption"]["user_key"]["value"].encode())).decode(
  198.                     "ascii"))
  199.     if "hex_value" in license["encryption"]["user_key"]:
  200.         password_hashes.append(
  201.             binascii.hexlify(bytearray.fromhex(license["encryption"]["user_key"]["hex_value"])).decode("ascii"))
  202.  
  203.     # Hash all the passwords provided by the user:
  204.     for possible_passphrase in passphrases:
  205.         algo = "http://www.w3.org/2001/04/xmlenc#sha256"
  206.         if "algorithm" in license["encryption"]["user_key"]:
  207.             algo = license["encryption"]["user_key"]["algorithm"]
  208.  
  209.         algo, tmp_pw = LCPTransform.userpass_to_hash(possible_passphrase.encode('utf-8'), algo)
  210.         if tmp_pw is not None:
  211.             password_hashes.append(tmp_pw)
  212.  
  213.     # For all the password hashes, check if one of them decrypts the book:
  214.     correct_password_hash = None
  215.  
  216.     for possible_hash in password_hashes:
  217.         transformed_hash = transform_algo(possible_hash)
  218.         try:
  219.             decrypted = None
  220.             decrypted = dataDecryptLCP(key_check, transformed_hash)
  221.         except:
  222.             pass
  223.  
  224.         if (decrypted is not None and decrypted.decode("ascii", errors="ignore") == license["id"]):
  225.             # Found correct password hash, hooray!
  226.             correct_password_hash = transformed_hash
  227.             break
  228.  
  229.     # Print an error message if none of the passwords worked
  230.     if (correct_password_hash is None):
  231.         print("LCP: Tried {0} passphrases, but none of them could decrypt the book ...".format(len(password_hashes)))
  232.  
  233.         # Print password hint, if available
  234.         if ("text_hint" in license["encryption"]["user_key"] and license["encryption"]["user_key"]["text_hint"] != ""):
  235.             print("LCP: The book distributor has given you the following passphrase hint: \"{0}\"".format(
  236.                 license["encryption"]["user_key"]["text_hint"]))
  237.  
  238.         print("LCP: Enter the correct passphrase in the DeDRM plugin settings, then try again.")
  239.  
  240.         # Print password reset instructions, if available
  241.         for link in license["links"]:
  242.             if ("rel" in link and link["rel"] == "hint"):
  243.                 print("LCP: You may be able to find or reset your LCP passphrase on the following webpage: {0}".format(
  244.                     link["href"]))
  245.                 break
  246.  
  247.         file.close()
  248.         raise LCPError("No correct passphrase found")
  249.  
  250.     print("LCP: Found correct passphrase, decrypting book ...")
  251.  
  252.     # Take the key we found and decrypt the content key:
  253.     decrypted_content_key = dataDecryptLCP(encrypted_content_key, correct_password_hash)
  254.  
  255.     if decrypted_content_key is None:
  256.         raise LCPError("Decrypted content key is None")
  257.  
  258.     # Begin decrypting
  259.     decryptor = Decryptor(decrypted_content_key)
  260.  
  261.     strippedPath = inpath.split("/")
  262.     strippedFile = strippedPath[len(strippedPath)-1].split(".")[0]
  263.     outputFolder = inpath[:-len(strippedPath[len(strippedPath)-1])] + strippedFile
  264.  
  265.     if not(os.path.exists(outputFolder)) or not(os.path.isdir(outputFolder)):
  266.         os.mkdir(outputFolder)
  267.  
  268.     manifest = json.loads(file.read('manifest.json'))
  269.     hrefs = [entry["href"] for entry in manifest["readingOrder"]]
  270.  
  271.     for chapter in hrefs:
  272.         pdfdata = file.read(chapter)
  273.         outputname = outputFolder + "/" + chapter
  274.  
  275.         with open(outputname, 'wb') as f:
  276.             f.write(decryptor.decrypt(pdfdata))
  277.  
  278.         print("LCP: Chapter successfully decrypted, exporting to {0}".format(outputname))
  279.  
  280.     for zipped in file.namelist():
  281.         if zipped.endswith(".png") or zipped.endswith(".jpg") or zipped.endswith(".jpeg"):
  282.             shutil.move(file.extract(zipped), outputFolder + "/" + zipped)
  283.             print("Cover art successfully copied to output folder")
  284.  
  285.     file.close()
  286.  
  287.  
  288.     #from pydub import AudioSegment
  289.     #counter = 0
  290.     #for file in os.listdir(outputFolder):
  291.     #    if file.endswith(".mp3") or file.endswith(".MP3"):
  292.     #        if counter < 4:
  293.     #            if 'combined' in locals():
  294.     #                combined = AudioSegment.from_file(outputFolder + "/" + file, format="mp3") + combined
  295.     #            else:
  296.     #                combined = AudioSegment.from_file(outputFolder + "/" + file, format="mp3")
  297.     #            counter += 1
  298.     #combined.export(outputFolder + "/onefile.mp3", format="mp3")
  299.  
  300.     return 0
  301.  
  302.  
  303. if __name__ == '__main__':
  304.     Run().start()
Add Comment
Please, Sign In to add comment