John Buckley
John Buckley

Reputation: 23

How to convert .xod file to .pdf/.xps PDFTron

I've use docpub to create a xod file with a pass to view in my PDFTron WebViewer application, but I ended up losing the original pdf file and now I would like to have it back. I have the xod encrypted file and the password. Can I convert it back to a pdf?

I tried to use the webviewer with react, but seems not possible to save the pdf file and docpub only has:
./DocPub –f xod --xod_encrypt_password secret license.pdf

Any ideas?

Upvotes: 2

Views: 3524

Answers (1)

Tomer
Tomer

Reputation: 3297

Yes, you can decrypt it manually and use the PDFNetPython library to convert it back to PDF:

import sys
import os
import zlib
import binascii
import struct
import argparse

from PDFNetPython3 import *
from Crypto.Cipher import AES

YOUR_PDFTRON_LICENSE_KEY = "<INSERT_LICENSE_KEY>"

def main():
    parser = argparse.ArgumentParser(description='Converts password-protected XOD files to PDF files')
    parser.add_argument('xod_file', type=str, help='path to the encrypted XOD file')
    parser.add_argument('password', type=str, help='the password to decrypt the file...')

    args = parser.parse_args()

    # alright.

    fileHandler = open(args.xod_file,"rb")
    password = args.password

    file = bytearray(fileHandler.read())

    new_file = bytearray()
    new_file[:] = file
    centeral_directory_end = file[-22:]
    centeral_directory_location = to_dword(centeral_directory_end[16:16+4])
    centeral_directory_size = to_dword(centeral_directory_end[12:12+4])
    centeral_directory = file[centeral_directory_location : centeral_directory_location + centeral_directory_size]

    print("[+] file loaded. starting to decrypt the file...")
    while len(centeral_directory) >= 42:
        file_name = centeral_directory[46:46 + to_short(centeral_directory[28:28+2])]
        start = to_dword(centeral_directory[42:42+4]) + 30 + len(file_name) + to_dword(centeral_directory[30:30+4])
        size = to_dword(centeral_directory[20:20+4])

        key, iv, encrypted_data = key_and_iv_from_password(bytearray(new_file[start:start+size]), file_name.decode("utf-8"), password)
        aes = AES.new(key, AES.MODE_CBC, iv)
        print("[+] decrypting part " + file_name.decode("utf-8") + " with key " + toHex(key))
        decrypted_data = aes.decrypt(encrypted_data)
        
        new_centeral_directory_index = fix_offsets(new_file, file_name, len(decrypted_data))
        del new_file[start:start+size]
        new_file[start:start] = bytearray(decrypted_data)
        centeral_directory = new_file[new_centeral_directory_index:]
        if centeral_directory[0] != 0x50:
            print("problem")
            break
        file_header_size = 46 + len(file_name) + to_short(centeral_directory[30:30+2]) + to_short(centeral_directory[32:32+2])
        centeral_directory = centeral_directory[file_header_size:]

    print("[+] everything decrypted. converting to pdf...")
    new_file_handler = open("new.xod","wb")
    new_file_handler.write(new_file)
    new_file_handler.close()
    toPdf("new.xod","result.pdf")
    os.remove("new.xod")
    print("[+] Done!")

# some helper functions

def toPdf(xodFilename, outputPdfName):
    PDFNet.Initialize(YOUR_PDFTRON_LICENSE_KEY)
    pdf_doc = PDFDoc()
    Convert.ToPdf(pdf_doc, xodFilename)
    pdf_doc.Save(outputPdfName, SDFDoc.e_remove_unused)
    pdf_doc.Close()

def toXod(pdfFilename, outputXodName):
    pdf_doc = PDFDoc()
    Convert.ToXod(pdfFilename, outputXodName)

def toHex(byte_array):
    if byte_array is str:
        byte_array = bytearray(byte_array)
    hex = str(binascii.hexlify(byte_array))
    formatted_hex = ':'.join(hex[i:i+2] for i in range(0, len(hex), 2))
    return formatted_hex

def to_dword(byte_array):
    return struct.unpack('I', byte_array)[0]

def to_short(byte_array):
    return struct.unpack('H', byte_array)[0]

def key_and_iv_from_password(encrypted_data, filename, password):
    key = bytearray([0] * 16)
    for i in range(16):
        key[i] = i
        if i < len(password):
            key[i] |= ord(password[i])
        g = len(filename) + i - 16
        if 0 <= g:
            key[i] |= ord(filename[g])
    
    iv = []
    for i in range(16):
        iv.append(encrypted_data[i])
    encrypted_data = encrypted_data[16:]

    return (key, bytearray(iv), encrypted_data)

def fix_offsets(array, alterted_filename, new_size):
    centeral_directory_end = array[-22:]
    centeral_directory_location = to_dword(centeral_directory_end[16:16+4])
    centeral_directory_size = to_dword(centeral_directory_end[12:12+4])
    file_offset_from_centeral_directory = 0
    index = 0
    offset_fix_delta = 0

    # fixing the alterted-file's local header
    while index < centeral_directory_location:   
        file_name = array[index + 30: index + 30 + to_short(array[index+26:index+26+2])]
        if file_name == alterted_filename:
            offset_fix_delta = new_size - to_dword(array[index + 18:index+18+4])
            array[index + 18:index+18+4] = struct.pack('I', new_size)
            break
        index += 30 + len(file_name) + to_short(array[index + 28:index+28+2]) + to_dword(array[index+18:index+18+4])

    # fixing the centeral directory
    index = centeral_directory_location
    should_fix_offset = False
    while index < centeral_directory_location + centeral_directory_size:  
        file_name = array[index + 46:index + 46 + to_short(array[index+28:index+28+2])]
        if file_name == alterted_filename:
            should_fix_offset = True
            array[index+20:index+20+4] = struct.pack('I', new_size)
            file_offset_from_centeral_directory = index-centeral_directory_location
        elif should_fix_offset:
            array[index +42:index+42+4] = struct.pack('I', to_dword(array[index +42:index+42+4]) + offset_fix_delta)
        file_header_size = 46 + len(file_name) + to_short(array[index+30:index+30+2]) + to_short(array[index+32:index+32+2])
        index += file_header_size

    array[-6:-6+4]= struct.pack('I', to_dword(array[-6:-6+4]) + offset_fix_delta)
    return to_dword(array[-6:-6+4]) + file_offset_from_centeral_directory

if __name__ == "__main__":
    main()

Make sure to install dependencies first:

pip3 install pycryptodome PDFNetPython3

And then just use it like so:

python decrypt_xod.py some_xod_file.xod MySecretPassword

Upvotes: 4

Related Questions