mirror of
https://github.com/Leseratte10/acsm-calibre-plugin.git
synced 2024-12-22 17:29:56 +06:00
297 lines
7.9 KiB
Python
297 lines
7.9 KiB
Python
'''
|
|
Copyright (c) 2021-2023 Leseratte10
|
|
This file is part of the ACSM Input Plugin by Leseratte10
|
|
ACSM Input Plugin for Calibre / acsm-calibre-plugin
|
|
|
|
For more information, see:
|
|
https://github.com/Leseratte10/acsm-calibre-plugin
|
|
'''
|
|
|
|
import sys, os, zlib, base64, time
|
|
|
|
class BackwardReader:
|
|
|
|
def __init__(self, file):
|
|
self.file = file
|
|
|
|
|
|
def readlines(self):
|
|
BLKSIZE = 4096
|
|
# Move reader to the end of file
|
|
self.file.seek(0, os.SEEK_END)
|
|
if sys.version_info[0] >= 3:
|
|
buffer = bytearray()
|
|
else:
|
|
buffer = ""
|
|
|
|
while True:
|
|
if sys.version_info[0] >= 3:
|
|
pos_newline = buffer.rfind(bytes([0x0a]))
|
|
else:
|
|
pos_newline = buffer.rfind("\n")
|
|
|
|
# Get the current position of the reader
|
|
current_pos = self.file.tell()
|
|
if pos_newline != -1:
|
|
# Newline is found
|
|
line = buffer[pos_newline+1:]
|
|
buffer = buffer[:pos_newline]
|
|
if sys.version_info[0] >= 3:
|
|
yield line.decode("latin-1")
|
|
else:
|
|
yield line
|
|
|
|
elif current_pos:
|
|
# Need to fill the buffer
|
|
to_read = min(BLKSIZE, current_pos)
|
|
self.file.seek(current_pos-to_read, 0)
|
|
buffer = self.file.read(to_read) + buffer
|
|
self.file.seek(current_pos-to_read, 0)
|
|
if current_pos is to_read:
|
|
if sys.version_info[0] >= 3:
|
|
buffer = bytes([0x0a]) + buffer
|
|
else:
|
|
buffer = "\n" + buffer
|
|
else:
|
|
# Start of file
|
|
return
|
|
|
|
|
|
|
|
|
|
def trim_encrypt_string(encrypt):
|
|
|
|
string_list = list(encrypt)
|
|
strlen = len(encrypt)
|
|
|
|
i = 0
|
|
bracket_count = 0
|
|
while (i < strlen):
|
|
if string_list[i] == "<" and string_list[i+1] == "<":
|
|
bracket_count += 1
|
|
|
|
if string_list[i] == ">" and string_list[i+1] == ">":
|
|
bracket_count -= 1
|
|
|
|
if bracket_count == 0:
|
|
break
|
|
|
|
i = i + 1
|
|
|
|
len_to_use = i+2
|
|
|
|
return encrypt[0:len_to_use]
|
|
|
|
def cleanup_encrypt_element(element):
|
|
|
|
if element.startswith("ID[<"):
|
|
element = element.replace("><", "> <")
|
|
|
|
element = ' '.join(element.split())
|
|
element = element.replace("[ ", "[").replace("] ", "]")
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
|
def deflate_and_base64_encode( string_val ):
|
|
zlibbed_str = zlib.compress( string_val )
|
|
compressed_string = zlibbed_str[2:-4]
|
|
return base64.b64encode( compressed_string )
|
|
|
|
def update_ebx_with_keys(ebx_data, adept_license, ebx_bookid):
|
|
|
|
b64data = deflate_and_base64_encode(adept_license.encode("utf-8")).decode("utf-8")
|
|
|
|
ebx_new = ebx_data[:-2]
|
|
ebx_new += "/EBX_BOOKID(%s)/ADEPT_LICENSE(%s)>>" % (ebx_bookid, b64data)
|
|
|
|
return ebx_new
|
|
|
|
|
|
def find_ebx(filename_in):
|
|
find_ebx_start = int(time.time() * 1000)
|
|
i = 0
|
|
|
|
fl = open(filename_in, "rb")
|
|
br = BackwardReader(fl)
|
|
|
|
for line in br.readlines():
|
|
i = i + 1
|
|
if "/EBX_HANDLER/" in line:
|
|
find_ebx_end = int(time.time() * 1000)
|
|
print("Found EBX after %d attempts - took %d ms" % (i, find_ebx_end - find_ebx_start))
|
|
return line
|
|
|
|
find_ebx_end = int(time.time() * 1000)
|
|
print("Error: Did not find EBX_HANDLER - took %d ms" % (find_ebx_end - find_ebx_start))
|
|
return None
|
|
|
|
def find_enc(filename_in):
|
|
find_enc_start = int(time.time() * 1000)
|
|
i = 0
|
|
|
|
fl = open(filename_in, "rb")
|
|
br = BackwardReader(fl)
|
|
|
|
for line in br.readlines():
|
|
i = i + 1
|
|
is_encrypt_normal = "R/Encrypt" in line and "R/ID" in line
|
|
is_encrypt_odd = "R" in line and "/Encrypt" in line and "/ID" in line
|
|
if is_encrypt_normal or is_encrypt_odd:
|
|
|
|
find_enc_end = int(time.time() * 1000)
|
|
print("Found ENC after %d attempts - took %d ms" % (i, find_enc_end - find_enc_start))
|
|
if is_encrypt_odd:
|
|
print("Odd formatting of encryption blob?")
|
|
print("If this doesn't work correctly please open a bug report.")
|
|
|
|
return line
|
|
|
|
find_enc_end = int(time.time() * 1000)
|
|
print("Error: Did not find ENC - took %d ms" % (find_enc_end - find_enc_start))
|
|
return None
|
|
|
|
|
|
|
|
def patch_drm_into_pdf(filename_in, adept_license_string, filename_out, ebx_bookid):
|
|
|
|
drm_start_time = int(time.time() * 1000)
|
|
|
|
trailer = ""
|
|
trailer_idx = 0
|
|
|
|
startxref_offset = 0
|
|
prevline = ""
|
|
|
|
|
|
fl = open(filename_in, "rb")
|
|
br = BackwardReader(fl)
|
|
|
|
print("Searching for startxref ...")
|
|
for line in br.readlines():
|
|
trailer_idx += 1
|
|
trailer = line + "\n" + trailer
|
|
|
|
#print ("LINE: " + line)
|
|
|
|
if (trailer_idx > 10):
|
|
print("Took more than 10 attempts to find startxref ...")
|
|
return False
|
|
|
|
if (line == "startxref"):
|
|
startxref_offset = int(prevline)
|
|
print("Got startxref: %d" % (startxref_offset))
|
|
break
|
|
prevline = line
|
|
|
|
|
|
|
|
r_encrypt_offs1 = 0
|
|
r_encrypt_offs2 = 0
|
|
|
|
encrypt = None
|
|
|
|
|
|
encrypt = find_enc(filename_in)
|
|
if encrypt is None:
|
|
print("Error, enc not found")
|
|
return False
|
|
|
|
line_split = encrypt.split(' ')
|
|
next = 0
|
|
for element in line_split:
|
|
if element == "R/Encrypt" or element == "/Encrypt" or element == "null/Encrypt":
|
|
next = 2
|
|
continue
|
|
if next == 2:
|
|
r_encrypt_offs1 = element
|
|
next = 1
|
|
continue
|
|
if next == 1:
|
|
r_encrypt_offs2 = element
|
|
next = 0
|
|
continue
|
|
|
|
|
|
# read EBX element:
|
|
ebx_elem = find_ebx(filename_in)
|
|
|
|
if (ebx_elem is None):
|
|
print("Err: EBX is None")
|
|
return False
|
|
|
|
|
|
print("")
|
|
print("")
|
|
print("Encryption handler:")
|
|
print(encrypt)
|
|
print("EBX handler:")
|
|
print(ebx_elem)
|
|
|
|
encrypt = trim_encrypt_string(encrypt)
|
|
|
|
print("Trimmed encryption handler:")
|
|
print(encrypt)
|
|
|
|
ebx_elem = update_ebx_with_keys(ebx_elem, adept_license_string, ebx_bookid)
|
|
|
|
print("Updated EBX handler not logged due to sensitive data")
|
|
#print(ebx_elem)
|
|
|
|
|
|
filesize_str = str(os.path.getsize(filename_in))
|
|
filesize_pad = filesize_str.zfill(10)
|
|
|
|
|
|
additional_data = "\r"
|
|
additional_data += r_encrypt_offs1 + " " + r_encrypt_offs2 + " " + "obj" + "\r"
|
|
additional_data += ebx_elem
|
|
additional_data += "\r"
|
|
additional_data += "endobj"
|
|
|
|
ptr = int(filesize_str) + len(additional_data)
|
|
|
|
additional_data += "\rxref\r" + r_encrypt_offs1 + " " + str((int(r_encrypt_offs2) + 1)) + "\r"
|
|
additional_data += filesize_pad + " 00000 n" + "\r\n"
|
|
additional_data += "trailer"
|
|
additional_data += "\r"
|
|
|
|
arr_root_str = encrypt.split('/')
|
|
did_prev = False
|
|
for elem in arr_root_str:
|
|
if elem.startswith("Prev"):
|
|
did_prev = True
|
|
additional_data += "Prev " + str(startxref_offset)
|
|
#print("Replacing prev from '%s' to '%s'" % (elem, "Prev " + startxref))
|
|
else:
|
|
additional_data += cleanup_encrypt_element(elem)
|
|
additional_data += "/"
|
|
|
|
if not did_prev:
|
|
# remove two >> at end
|
|
additional_data = additional_data[:-3]
|
|
additional_data += "/Prev " + str(startxref_offset) + ">>" + "/"
|
|
#print("Faking Prev %s" % startxref)
|
|
|
|
additional_data = additional_data[:-1]
|
|
|
|
additional_data += "\r" + "startxref\r" + str(ptr) + "\r" + "%%EOF"
|
|
|
|
#print("Appending DRM data: %s" % (additional_data))
|
|
|
|
|
|
inp = open(filename_in, "rb")
|
|
|
|
out = open(filename_out, "wb")
|
|
out.write(inp.read())
|
|
out.write(additional_data.encode("latin-1"))
|
|
inp.close()
|
|
out.close()
|
|
|
|
drm_end_time = int(time.time() * 1000)
|
|
|
|
print("Whole DRM patching took %d milliseconds." % (drm_end_time - drm_start_time))
|
|
|
|
return True |