Repository: apprenticeharper/DeDRM_tools Branch: master Commit: 776f146ca00d Files: 109 Total size: 1.4 MB Directory structure: gitextract_cqmmgc0y/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── QUESTION.md │ └── workflows/ │ ├── Format.yaml │ └── Lint.yaml ├── .gitignore ├── CALIBRE_CLI_INSTRUCTIONS.md ├── DeDRM_plugin/ │ ├── DeDRM_Adobe Digital Editions Key_Help.htm │ ├── DeDRM_Barnes and Noble Key_Help.htm │ ├── DeDRM_EInk Kindle Serial Number_Help.htm │ ├── DeDRM_Help.htm │ ├── DeDRM_Kindle for Android Key_Help.htm │ ├── DeDRM_Kindle for Mac and PC Key_Help.htm │ ├── DeDRM_Mobipocket PID_Help.htm │ ├── DeDRM_eReader Key_Help.htm │ ├── __init__.py │ ├── activitybar.py │ ├── adobekey.py │ ├── aescbc.py │ ├── alfcrypto.py │ ├── androidkindlekey.py │ ├── argv_utils.py │ ├── askfolder_ed.py │ ├── config.py │ ├── convert2xml.py │ ├── epubtest.py │ ├── erdr2pml.py │ ├── flatxml2html.py │ ├── flatxml2svg.py │ ├── genbook.py │ ├── ignobleepub.py │ ├── ignoblekey.py │ ├── ignoblekeyfetch.py │ ├── ignoblekeygen.py │ ├── ignoblepdf.py │ ├── ineptepub.py │ ├── ineptpdf.py │ ├── ion.py │ ├── k4mobidedrm.py │ ├── kfxdedrm.py │ ├── kgenpids.py │ ├── kindlekey.py │ ├── kindlepid.py │ ├── mobidedrm.py │ ├── openssl_des.py │ ├── plugin-import-name-dedrm.txt │ ├── prefs.py │ ├── pycrypto_des.py │ ├── python_des.py │ ├── scriptinterface.py │ ├── scrolltextwidget.py │ ├── simpleprefs.py │ ├── stylexml2css.py │ ├── subasyncio.py │ ├── topazextract.py │ ├── utilities.py │ ├── wineutils.py │ ├── zipfilerugged.py │ └── zipfix.py ├── DeDRM_plugin_ReadMe.txt ├── FAQs.md ├── Obok_plugin/ │ ├── __init__.py │ ├── action.py │ ├── common_utils.py │ ├── config.py │ ├── dialogs.py │ ├── obok/ │ │ ├── __init__.py │ │ ├── legacy_obok.py │ │ └── obok.py │ ├── obok_dedrm_Help.htm │ ├── plugin-import-name-obok_dedrm.txt │ ├── translations/ │ │ ├── ar.mo │ │ ├── ar.po │ │ ├── de.mo │ │ ├── de.po │ │ ├── default.po │ │ ├── es.mo │ │ ├── es.po │ │ ├── nl.mo │ │ ├── nl.po │ │ ├── pt.mo │ │ ├── pt.po │ │ ├── sv.mo │ │ └── sv.po │ └── utilities.py ├── Other_Tools/ │ ├── B_and_N_Download_Helper/ │ │ ├── BN-Dload.user.js │ │ └── BN-Dload.user_ReadMe.txt │ ├── DRM_Key_Scripts/ │ │ ├── Adobe_Digital_Editions/ │ │ │ └── adobekey.pyw │ │ ├── Barnes_and_Noble_ePubs/ │ │ │ ├── ignoblekey.pyw │ │ │ ├── ignoblekeyfetch.pyw │ │ │ └── ignoblekeygen.pyw │ │ ├── Kindle_for_Android/ │ │ │ └── androidkindlekey.pyw │ │ ├── Kindle_for_Mac_and_PC/ │ │ │ └── kindlekey.pyw │ │ └── Kindle_for_iOS/ │ │ └── kindleiospidgen.pyw │ ├── Kindle_for_Android_Patches/ │ │ ├── A_Patching_Experience.txt │ │ ├── kindle_version_3.0.1.70/ │ │ │ ├── ReadMe_K4Android.txt │ │ │ └── kindle3.0.1.70.patch │ │ ├── kindle_version_3.7.0.108/ │ │ │ ├── ReadMe_K4Android.txt │ │ │ └── kindle3.7.0.108.patch │ │ ├── kindle_version_4.0.2.1/ │ │ │ └── kindle4.0.2.1.patch │ │ └── kindle_version_4.8.1.10/ │ │ ├── Notes on the Patch.txt │ │ └── kindle4.8.1.10.patch │ ├── Kobo/ │ │ └── obok.py │ ├── Rocket_ebooks/ │ │ └── rebhack_ReadMe.txt │ ├── Scuolabook_DRM/ │ │ └── Scuolabook_ReadMe.txt │ └── Tetrachroma_FileOpen_ineptpdf/ │ ├── ineptpdf_8.4.51.pyw │ └── ineptpdf_8.4.51_ReadMe.txt ├── README.md ├── ReadMe_Overview.txt ├── make_release.py └── obok_plugin_ReadMe.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/QUESTION.md ================================================ --- name: Question about: Questions for DeDRM Project title: "[QUESTION] Title" labels: Question --- ## CheckList - [ ] `The Title` and The `Log Title` are setted correctly. - [ ] Clarified about `my environment`. - [ ] Code block is used for `the log`. --- ## Title ## My Environment ### Calibre: `Version` ### Kindle: `Version` ### DeDRM: `Version` ## Log
Log Title ```log PUT YOUR LOG ```
================================================ FILE: .github/workflows/Format.yaml ================================================ name: Python code format on: push: branches: master jobs: Format: if: "contains(github.event.head_commit.message, '!format')" runs-on: ubuntu-20.04 strategy: fail-fast: false steps: - uses: actions/checkout@main - name: Set up Python uses: actions/setup-python@main with: python-version: 3.x - uses: actions/cache@main with: path: ~/.cache/pip key: ${{ runner.os }}-pip-format restore-keys: | ${{ runner.os }}-pip-format - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install autopep8 pycodestyle - name: Format by autopep8 then Push env: GIT_EMAIL: github-actions[bot]@users.noreply.github.com GIT_ACTOR: github-actions[bot] run: | export HASH_SHORT=$(git rev-parse --short HEAD) git checkout -b format--${HASH_SHORT} git config --global user.email $GIT_EMAIL git config --global user.name $GIT_ACTOR python -m autopep8 --in-place --aggressive --aggressive --experimental -r ./ git add -A git commit -m 'Format by autopep8' -m From: -m $(git rev-parse HEAD) git push --set-upstream origin format--${HASH_SHORT} ================================================ FILE: .github/workflows/Lint.yaml ================================================ name: Python code review on: [push, pull_request] jobs: Test: runs-on: ubuntu-20.04 strategy: fail-fast: false steps: - uses: actions/checkout@main - name: Set up Python uses: actions/setup-python@main with: python-version: 3.x - uses: actions/cache@main with: path: ~/.cache/pip key: ${{ runner.os }}-pip-lint restore-keys: | ${{ runner.os }}-pip-lint - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 - name: Lint with flake8 run: | python -m flake8 . --builtins=_,I --ignore=E501 --count --benchmark --show-source --statistics ================================================ FILE: .gitignore ================================================ .DS_Store ================================================ FILE: CALIBRE_CLI_INSTRUCTIONS.md ================================================ # Using the DeDRM plugin with the Calibre command line interface If you prefer the Calibre CLI instead of the GUI, follow this guide to install and use the DeDRM plugin. This guide assumes you are on Linux, but it may very well work on other platforms. ## Step-by-step Tutorial #### Install Calibre - Follow [Calibre's installation instructions](https://calibre-ebook.com/download_linux) #### Install plugins - Download the DeDRM `.zip` archive from DeDRM_tools' [latest release](https://github.com/apprenticeharper/DeDRM_tools/releases/latest). Then unzip it. - Add the DeDRM plugin to Calibre: ``` cd *the unzipped DeDRM_tools folder* calibre-customize --add DeDRM_calibre_plugin/DeDRM_plugin.zip ``` - Add the Obok plugin: ``` calibre-customize --add Obok_calibre_plugin/obok_plugin.zip ``` #### Enter your keys - Figure out what format DeDRM wants your key in by looking in [the code that handles that](DeDRM_plugin/prefs.py). - For Kindle eInk devices, DeDRM expects you to put a list of serial numbers in the `serials` field: `"serials": ["012345689abcdef"]` or `"serials": ["1111111111111111", "2222222222222222"]`. - Now add your keys to `$CALIBRE_CONFIG_DIRECTORY/plugins/dedrm.json`. #### Import your books - Make a library folder ``` mkdir library ``` - Add your book(s) with this command: ``` calibredb add /path/to/book.format --with-library=library ``` The DRM should be removed from your book, which you can find in the `library` folder. ================================================ FILE: DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm ================================================ Managing Adobe Digital Editions Keys

Managing Adobe Digital Editions Keys

If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.

Creating New Keys:

On the right-hand side of the plugin’s 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 prompting you to enter a key name for the default Adobe Digital Editions key.

Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you don’t want to create the key.

New keys are checked against the current list of keys before being added, and duplicates are discarded.

Deleting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Renaming Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

Exporting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

Linux Users: WINEPREFIX

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Adobe Digital Editions under Wine, and your wine installation containing Adobe Digital Editions isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

Importing Existing Keyfiles:

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.der’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.

================================================ FILE: DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm ================================================ Managing Barnes and Noble Keys

Managing Barnes and Noble Keys

If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

Changes at Barnes & Noble

In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.

Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.

Creating New Keys:

On the right-hand side of the plugin’s 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 the necessary data to generate a new key.

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

New keys are checked against the current list of keys before being added, and duplicates are discarded.

Deleting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Renaming Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

Exporting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b64’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

Importing Existing Keyfiles:

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.

NOOK Study

Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.

================================================ FILE: DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm ================================================  Managing eInk Kindle serial numbers

Managing eInk Kindle serial numbers

If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.

Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.

Creating New Kindle serial numbers:

On the right-hand side of the plugin’s 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 Kindle serial number.

Click the OK button to save the serial number. Or Cancel if you didn’t want to enter a serial number.

Deleting Kindle serial numbers:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.

================================================ FILE: DeDRM_plugin/DeDRM_Help.htm ================================================ DeDRM Plugin Configuration

DeDRM Plugin (v6.7.0)

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

Installation

You have obviously managed to install the plugin, as otherwise you wouldn’t be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).

Configuration

On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)

If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.

If you have used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.

When you have finished entering your configuration information, you must click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.

Troubleshooting:

If you find that it’s not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.

Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.

Note: The Mac version of Calibre doesn’t install the command line tools by default. If you go to the ‘Preferences’ page and click on the miscellaneous button, you’ll find the option to install the command line tools.

Credits:

For additional help read the FAQs at Apprentice Harpers’s GitHub repository. You can ask questions in the comments section of the first post at Apprentice Alf's blog or raise an issue.

Linux Systems Only

Generating decryption keys for Adobe Digital Editions and Kindle for PC

If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.

To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)

Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.

Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.

================================================ FILE: DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm ================================================  Managing Kindle for Android Keys

Managing Kindle for Android Keys

Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.

Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.

Getting the Kindle for Android backup file

Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.

Enable developer mode on your Android device. Again, look for an on-line guide for your device.

Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.

Adding a Kindle for Android Key

On the right-hand side of the plugin’s 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 with two main controls.

Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you don’t want to store the key.

New keys are checked against the current list of keys before being added, and duplicates are discarded.

Deleting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Renaming Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.

Exporting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

Importing Existing Keyfiles:

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.

================================================ FILE: DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm ================================================ Managing Kindle for Mac/PC Keys

Managing Kindle for Mac/PC Keys

If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.

Creating New Keys:

On the right-hand side of the plugin’s 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 prompting you to enter a key name for the default Kindle for Mac/PC key.

Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you don’t want to create the key.

New keys are checked against the current list of keys before being added, and duplicates are discarded.

Deleting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Renaming Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

Exporting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

Linux Users: WINEPREFIX

Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are using the Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.

Importing Existing Keyfiles:

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.k4i’ key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

================================================ FILE: DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm ================================================ Managing Mobipocket PIDs

Managing Mobipocket PIDs

If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.

Creating New Mobipocket PIDs:

On the right-hand side of the plugin’s 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 Mobipocket PID.

Click the OK button to save the PID. Or Cancel if you didn’t want to enter a PID.

Deleting Mobipocket PIDs:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

================================================ FILE: DeDRM_plugin/DeDRM_eReader Key_Help.htm ================================================ Managing eReader Keys

Managing eReader Keys

If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise ‘.pdb’) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.

Creating New Keys:

On the right-hand side of the plugin’s 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 the necessary data to generate a new key.

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

New keys are checked against the current list of keys before being added, and duplicates are discarded.

Deleting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.

Renaming Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..

Exporting Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b63’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

Importing Existing Keyfiles:

At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b63’ key files that have previously been exported.

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

================================================ FILE: DeDRM_plugin/__init__.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # __init__.py for DeDRM_plugin # Copyright © 2008-2020 Apprentice Harper et al. __license__ = 'GPL v3' __version__ = '7.2.1' __docformat__ = 'restructuredtext en' # Released under the terms of the GNU General Public Licence, version 3 # # # All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts. # We had the much easier job of converting them to a calibre plugin. # # This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs, # Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having # to install any dependencies... other than having calibre installed, of course. # # Configuration: # Check out the plugin's configuration settings by clicking the "Customize plugin" # button when you have the "DeDRM" plugin highlighted (under Preferences-> # Plugins->File type plugins). Once you have the configuration dialog open, you'll # see a Help link on the top right-hand side. # # Revision history: # 6.0.0 - Initial release # 6.0.1 - Bug Fixes for Windows App, Kindle for Mac and Windows Adobe Digital Editions # 6.0.2 - Restored call to Wine to get Kindle for PC keys, added for ADE # 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names # 6.0.4 - Fixes for stand-alone scripts and applications # and pdb files in plugin and initial conversion of prefs. # 6.0.5 - Fix a key issue # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support # 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. # 6.1.0 - Fixed multiple books import problem and PDF import with no key problem # 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs. # Fix for not copying needed files. Fix for getting default Adobe key for PDFs # 6.2.1 - Fix for non-ascii Windows user names # 6.2.2 - Added URL method for B&N/nook books # 6.3.0 - Added in Kindle for Android serial number solution # 6.3.1 - Version number bump for clarity # 6.3.2 - Fixed Kindle for Android help file # 6.3.3 - Bug fix for Kindle for PC support # 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17 # 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging # 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5 # 6.4.0 - Updated for new Kindle for PC encryption # 6.4.1 - Fix for some new tags in Topaz ebooks. # 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks # 6.4.3 - Fix for error that only appears when not in debug mode # Also includes fix for Macs with bonded ethernet ports # 6.5.0 - Big update to Macintosh app # Fix for some more 'new' tags in Topaz ebooks. # Fix an error in wineutils.py # 6.5.1 - Updated version number, added PDF check for DRM-free documents # 6.5.2 - Another Topaz fix # 6.5.3 - Warn about KFX files explicitly # 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython # 6.5.5 - Finally a fix for the Windows non-ASCII user names. # 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original # imported format was azw8 since that may be converted to kfx) # 6.6.1 - Thanks to wzyboy for a fix for stand-alone tools, and the new folder structure. # 6.6.2 - revamp of folders to get Mac OS X app working. Updated to 64-bit app. Various fixes. # 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018 # 6.7.0 - Handle new library in calibre. # 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+) # 6.8.1 - Kindle key fix for Mac OS X Big Sur # 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who contributed # 7.0.1 - More Python 3 changes. Adobe PDF decryption should now work in some cases # 7.0.2 - More Python 3 changes. Adobe PDF decryption should now work on PC too. # 7.0.3 - More Python 3 changes. Integer division in ineptpdf.py # 7.1.0 - Full release for calibre 5.x # 7.2.0 - Update for latest KFX changes, and Python 3 Obok fixes. # 7.2.1 - Whitespace! """ Decrypt DRMed ebooks. """ PLUGIN_NAME = "DeDRM" PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")]) PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' import codecs import sys, os, re import time import zipfile import traceback from zipfile import ZipFile class DeDRMError(Exception): pass from calibre.customize import FileTypePlugin from calibre.constants import iswindows, isosx from calibre.gui2 import is_ok_to_use_qt from calibre.utils.config import config_dir # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get safely # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data,str): data = data.encode(self.encoding,"replace") try: self.stream.buffer.write(data) self.stream.buffer.flush() except: # We can do nothing if a write fails pass def __getattr__(self, attr): return getattr(self.stream, attr) class DeDRM(FileTypePlugin): name = PLUGIN_NAME description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts." supported_platforms = ['linux', 'osx', 'windows'] author = "Apprentice Alf, Aprentice Harper, The Dark Reverser and i♥cabbages" version = PLUGIN_VERSION_TUPLE minimum_calibre_version = (5, 0, 0) # Python 3. file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip']) on_import = True on_preprocess = True priority = 600 def initialize(self): """ Dynamic modules can't be imported/loaded from a zipfile. So this routine will extract the appropriate library for the target OS and copy it to the 'alfcrypto' subdirectory of calibre's configuration directory. That 'alfcrypto' directory is then inserted into the syspath (as the very first entry) in the run function so the CDLL stuff will work in the alfcrypto.py script. The extraction only happens once per version of the plugin Also perform upgrade of preferences once per version """ try: self.pluginsdir = os.path.join(config_dir,"plugins") if not os.path.exists(self.pluginsdir): os.mkdir(self.pluginsdir) self.maindir = os.path.join(self.pluginsdir,"DeDRM") if not os.path.exists(self.maindir): os.mkdir(self.maindir) self.helpdir = os.path.join(self.maindir,"help") if not os.path.exists(self.helpdir): os.mkdir(self.helpdir) self.alfdir = os.path.join(self.maindir,"libraryfiles") if not os.path.exists(self.alfdir): os.mkdir(self.alfdir) # only continue if we've never run this version of the plugin before self.verdir = os.path.join(self.maindir,PLUGIN_VERSION) if not os.path.exists(self.verdir): if iswindows: names = ["alfcrypto.dll","alfcrypto64.dll"] elif isosx: names = ["libalfcrypto.dylib"] else: names = ["libalfcrypto32.so","libalfcrypto64.so","kindlekey.py","adobekey.py","subasyncio.py"] lib_dict = self.load_resources(names) print("{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION)) for entry, data in lib_dict.items(): file_path = os.path.join(self.alfdir, entry) try: os.remove(file_path) except: pass try: open(file_path,'wb').write(data) except: print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)) traceback.print_exc() pass # convert old preferences, if necessary. from calibre_plugins.dedrm.prefs import convertprefs convertprefs() # mark that this version has been initialized os.mkdir(self.verdir) except Exception as e: traceback.print_exc() raise def ePubDecrypt(self,path_to_ebook): # Create a TemporaryPersistent file to work with. # Check original epub archive for zip errors. import calibre_plugins.dedrm.zipfix inf = self.temporary_file(".epub") try: print("{0} v{1}: Verifying zip archive integrity".format(PLUGIN_NAME, PLUGIN_VERSION)) fr = zipfix.fixZip(path_to_ebook, inf.name) fr.fix() except Exception as e: print("{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])) raise Exception(e) # import the decryption keys import calibre_plugins.dedrm.prefs as prefs dedrmprefs = prefs.DeDRM_Prefs() # import the Barnes & Noble ePub handler import calibre_plugins.dedrm.ignobleepub as ignobleepub #check the book if ignobleepub.ignobleBook(inf.name): print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) # Attempt to decrypt epub with each encryption key (generated or provided). for keyname, userkey in dedrmprefs['bandnkeys'].items(): keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname) print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)) of = self.temporary_file(".epub") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ignobleepub.decryptBook(userkey, inf.name, of.name) except: print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 of.close() if result == 0: # Decryption was successful. # Return the modified PersistentTemporary file to calibre. return of.name print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)) # perhaps we should see if we can get a key from a log file print("{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) # get the default NOOK Study keys defaultkeys = [] try: if iswindows or isosx: from calibre_plugins.dedrm.ignoblekey import nookkeys defaultkeys = nookkeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,"ignoblekey.py") defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix']) except: print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() newkeys = [] for keyvalue in defaultkeys: if keyvalue not in dedrmprefs['bandnkeys'].values(): newkeys.append(keyvalue) if len(newkeys) > 0: try: for i,userkey in enumerate(newkeys): print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) of = self.temporary_file(".epub") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ignobleepub.decryptBook(userkey, inf.name, of.name) except: print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 of.close() if result == 0: # Decryption was a success # Store the new successful key in the defaults print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) try: dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue) dedrmprefs.writeprefs() print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except: print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() # Return the modified PersistentTemporary file to calibre. return of.name print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except Exception as e: pass 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)) 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)) # import the Adobe Adept ePub handler import calibre_plugins.dedrm.ineptepub as ineptepub if ineptepub.adeptBook(inf.name): print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) # Attempt to decrypt epub with each encryption key (generated or provided). for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): userkey = codecs.decode(userkeyhex, 'hex') print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)) of = self.temporary_file(".epub") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ineptepub.decryptBook(userkey, inf.name, of.name) except: print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 try: of.close() except: print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) if result == 0: # Decryption was successful. # Return the modified PersistentTemporary file to calibre. print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime)) return of.name 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)) # perhaps we need to get a new default ADE key print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) # get the default Adobe keys defaultkeys = [] try: if iswindows or isosx: from calibre_plugins.dedrm.adobekey import adeptkeys defaultkeys = adeptkeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,"adobekey.py") defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix']) self.default_key = defaultkeys[0] except: print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() self.default_key = "" newkeys = [] for keyvalue in defaultkeys: if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values(): newkeys.append(keyvalue) if len(newkeys) > 0: try: for i,userkey in enumerate(newkeys): print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) of = self.temporary_file(".epub") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ineptepub.decryptBook(userkey, inf.name, of.name) except: print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 of.close() if result == 0: # Decryption was a success # Store the new successful key in the defaults print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) try: dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',codecs.encode(keyvalue, 'hex').decode('ascii')) dedrmprefs.writeprefs() print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except: print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) # Return the modified PersistentTemporary file to calibre. return of.name print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except Exception as e: print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() pass # Something went wrong with decryption. 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)) 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)) # Not a Barnes & Noble nor an Adobe Adept # Import the fixed epub. print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) def PDFDecrypt(self,path_to_ebook): import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.ineptpdf dedrmprefs = prefs.DeDRM_Prefs() # Attempt to decrypt epub with each encryption key (generated or provided). print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): userkey = codecs.decode(userkeyhex,'hex') print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)) of = self.temporary_file(".pdf") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) except: print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 of.close() if result == 0: # Decryption was successful. # Return the modified PersistentTemporary file to calibre. return of.name 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)) # perhaps we need to get a new default ADE key print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) # get the default Adobe keys defaultkeys = [] try: if iswindows or isosx: from calibre_plugins.dedrm.adobekey import adeptkeys defaultkeys = adeptkeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,"adobekey.py") defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix']) self.default_key = defaultkeys[0] except: print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() self.default_key = "" newkeys = [] for keyvalue in defaultkeys: if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values(): newkeys.append(keyvalue) if len(newkeys) > 0: try: for i,userkey in enumerate(newkeys): print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) of = self.temporary_file(".pdf") # Give the user key, ebook and TemporaryPersistent file to the decryption function. try: result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) except: print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() result = 1 of.close() if result == 0: # Decryption was a success # Store the new successful key in the defaults print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) try: dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',codecs.encode(keyvalue,'hex')) dedrmprefs.writeprefs() print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except: print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() # Return the modified PersistentTemporary file to calibre. return of.name print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) except Exception as e: pass # Something went wrong with decryption. 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)) 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)) def KindleMobiDecrypt(self,path_to_ebook): # add the alfcrypto directory to sys.path so alfcrypto.py # will be able to locate the custom lib(s) for CDLL import. sys.path.insert(0, self.alfdir) # Had to move this import here so the custom libs can be # extracted to the appropriate places beforehand these routines # look for them. import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.k4mobidedrm dedrmprefs = prefs.DeDRM_Prefs() pids = dedrmprefs['pids'] serials = dedrmprefs['serials'] for android_serials_list in dedrmprefs['androidkeys'].values(): #print android_serials_list serials.extend(android_serials_list) #print serials androidFiles = [] kindleDatabases = list(dedrmprefs['kindlekeys'].items()) try: book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime) except Exception as e: decoded = False # perhaps we need to get a new default Kindle for Mac/PC key defaultkeys = [] print("{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0])) print("{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) try: if iswindows or isosx: from calibre_plugins.dedrm.kindlekey import kindlekeys defaultkeys = kindlekeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(self.alfdir,"kindlekey.py") defaultkeys = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix']) except: print("{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) traceback.print_exc() pass newkeys = {} for i,keyvalue in enumerate(defaultkeys): keyname = "default_key_{0:d}".format(i+1) if keyvalue not in dedrmprefs['kindlekeys'].values(): newkeys[keyname] = keyvalue if len(newkeys) > 0: print("{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys")) try: book = k4mobidedrm.GetDecryptedBook(path_to_ebook,list(newkeys.items()),[],[],[],self.starttime) decoded = True # store the new successful keys in the defaults print("{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys")) for keyvalue in newkeys.values(): dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue) dedrmprefs.writeprefs() except Exception as e: pass if not decoded: #if you reached here then no luck raise and exception 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)) 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)) of = self.temporary_file(book.getBookExtension()) book.getFile(of.name) of.close() book.cleanup() return of.name def eReaderDecrypt(self,path_to_ebook): import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.erdr2pml dedrmprefs = prefs.DeDRM_Prefs() # Attempt to decrypt epub with each encryption key (generated or provided). for keyname, userkey in dedrmprefs['ereaderkeys'].items(): keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname) print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)) of = self.temporary_file(".pmlz") # Give the userkey, ebook and TemporaryPersistent file to the decryption function. result = erdr2pml.decryptBook(path_to_ebook, of.name, True, codecs.decode(userkey,'hex')) of.close() # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. if result == 0: print("{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)) return of.name print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)) 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)) 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)) def run(self, path_to_ebook): # make sure any unicode output gets converted safely with 'replace' sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) print("{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) self.starttime = time.time() booktype = os.path.splitext(path_to_ebook)[1].lower()[1:] if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']: # Kindle/Mobipocket decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook) elif booktype == 'pdb': # eReader decrypted_ebook = self.eReaderDecrypt(path_to_ebook) pass elif booktype == 'pdf': # Adobe Adept PDF (hopefully) decrypted_ebook = self.PDFDecrypt(path_to_ebook) pass elif booktype == 'epub': # Adobe Adept or B&N ePub decrypted_ebook = self.ePubDecrypt(path_to_ebook) else: print("Unknown booktype {0}. Passing back to calibre unchanged".format(booktype)) return path_to_ebook print("{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) return decrypted_ebook def is_customizable(self): # return true to allow customization via the Plugin->Preferences. return True def config_widget(self): import calibre_plugins.dedrm.config as config return config.ConfigWidget(self.plugin_path, self.alfdir) def save_settings(self, config_widget): config_widget.save_settings() ================================================ FILE: DeDRM_plugin/activitybar.py ================================================ import sys import tkinter import tkinter.constants class ActivityBar(tkinter.Frame): def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\ bd=2, relief=tkinter.constants.GROOVE, *args, **kw): tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw) self._master = master self._interval = interval self._maximum = length self._startx = 0 self._barwidth = barwidth self._bardiv = length / barwidth if self._bardiv < 10: self._bardiv = 10 stopx = self._startx + self._barwidth if stopx > self._maximum: stopx = self._maximum # self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ # highlightthickness=0, relief='flat', bd=0) self._canv = tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ highlightthickness=0, relief=relief, bd=bd) self._canv.pack(fill='both', expand=1) self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0) self._set() self.bind('', self._update_coords) self._running = False def _update_coords(self, event): '''Updates the position of the rectangle inside the canvas when the size of the widget gets changed.''' # looks like we have to call update_idletasks() twice to make sure # to get the results we expect self._canv.update_idletasks() self._maximum = self._canv.winfo_width() self._startx = 0 self._barwidth = self._maximum / self._bardiv if self._barwidth < 2: self._barwidth = 2 stopx = self._startx + self._barwidth if stopx > self._maximum: stopx = self._maximum self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height()) self._canv.update_idletasks() def _set(self): if self._startx < 0: self._startx = 0 if self._startx > self._maximum: self._startx = self._startx % self._maximum stopx = self._startx + self._barwidth if stopx > self._maximum: stopx = self._maximum self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height()) self._canv.update_idletasks() def start(self): self._running = True self.after(self._interval, self._step) def stop(self): self._running = False self._set() def _step(self): if self._running: stepsize = self._barwidth / 4 if stepsize < 2: stepsize = 2 self._startx += stepsize self._set() self.after(self._interval, self._step) ================================================ FILE: DeDRM_plugin/adobekey.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # adobekey.pyw, version 6.0 # Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Revision history: # 1 - Initial release, for Adobe Digital Editions 1.7 # 2 - Better algorithm for finding pLK; improved error handling # 3 - Rename to INEPT # 4 - Series of changes by joblack (and others?) -- # 4.1 - quick beta fix for ADE 1.7.2 (anon) # 4.2 - added old 1.7.1 processing # 4.3 - better key search # 4.4 - Make it working on 64-bit Python # 5 - Clean up and improve 4.x changes; # Clean up and merge OS X support by unknown # 5.1 - add support for using OpenSSL on Windows in place of PyCrypto # 5.2 - added support for output of key to a particular file # 5.3 - On Windows try PyCrypto first, OpenSSL next # 5.4 - Modify interface to allow use of import # 5.5 - Fix for potential problem with PyCrypto # 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code # 5.7 - Unicode support added, renamed adobekey from ineptkey # 5.8 - Added getkey interface for Windows DeDRM application # 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility # 6.0 - Work if TkInter is missing # 7.0 - Python 3 for calibre 5 """ Retrieve Adobe ADEPT user key. """ __license__ = 'GPL v3' __version__ = '7.0' import sys, os, struct, getopt from base64 import b64decode # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["adobekey.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class ADEPTError(Exception): pass if iswindows: from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \ c_long, c_ulong from ctypes.wintypes import LPVOID, DWORD, BOOL import winreg def _load_crypto_libcrypto(): from ctypes.util import find_library libcrypto = find_library('libcrypto-1_1') if libcrypto is None: libcrypto = find_library('libeay32') if libcrypto is None: raise ADEPTError('libcrypto not found') libcrypto = CDLL(libcrypto) AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class AES(object): def __init__(self, userkey): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise ADEPTError('AES improper key used') key = self._key = AES_KEY() rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: raise ADEPTError('Failed to initialize AES key') def decrypt(self, data): out = create_string_buffer(len(data)) iv = (b"\x00" * self._blocksize) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: raise ADEPTError('AES decryption failed') return out.raw return AES def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES class AES(object): def __init__(self, key): self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16) def decrypt(self, data): return self._aes.decrypt(data) return AES def _load_crypto(): AES = None for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto): try: AES = loader() break except (ImportError, ADEPTError): pass return AES AES = _load_crypto() DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device' PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' MAX_PATH = 255 kernel32 = windll.kernel32 advapi32 = windll.advapi32 crypt32 = windll.crypt32 def GetSystemDirectory(): GetSystemDirectoryW = kernel32.GetSystemDirectoryW GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] GetSystemDirectoryW.restype = c_uint def GetSystemDirectory(): buffer = create_unicode_buffer(MAX_PATH + 1) GetSystemDirectoryW(buffer, len(buffer)) return buffer.value return GetSystemDirectory GetSystemDirectory = GetSystemDirectory() def GetVolumeSerialNumber(): GetVolumeInformationW = kernel32.GetVolumeInformationW GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, POINTER(c_uint), POINTER(c_uint), POINTER(c_uint), c_wchar_p, c_uint] GetVolumeInformationW.restype = c_uint def GetVolumeSerialNumber(path): vsn = c_uint(0) GetVolumeInformationW( path, None, 0, byref(vsn), None, None, None, 0) return vsn.value return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() def GetUserName(): GetUserNameW = advapi32.GetUserNameW GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] GetUserNameW.restype = c_uint def GetUserName(): buffer = create_unicode_buffer(32) size = c_uint(len(buffer)) while not GetUserNameW(buffer, byref(size)): buffer = create_unicode_buffer(len(buffer) * 2) size.value = len(buffer) return buffer.value.encode('utf-16-le')[::2] return GetUserName GetUserName = GetUserName() PAGE_EXECUTE_READWRITE = 0x40 MEM_COMMIT = 0x1000 MEM_RESERVE = 0x2000 def VirtualAlloc(): _VirtualAlloc = kernel32.VirtualAlloc _VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD] _VirtualAlloc.restype = LPVOID def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE), protect=PAGE_EXECUTE_READWRITE): return _VirtualAlloc(addr, size, alloctype, protect) return VirtualAlloc VirtualAlloc = VirtualAlloc() MEM_RELEASE = 0x8000 def VirtualFree(): _VirtualFree = kernel32.VirtualFree _VirtualFree.argtypes = [LPVOID, c_size_t, DWORD] _VirtualFree.restype = BOOL def VirtualFree(addr, size=0, freetype=MEM_RELEASE): return _VirtualFree(addr, size, freetype) return VirtualFree VirtualFree = VirtualFree() class NativeFunction(object): def __init__(self, restype, argtypes, insns): self._buf = buf = VirtualAlloc(None, len(insns)) memmove(buf, insns, len(insns)) ftype = CFUNCTYPE(restype, *argtypes) self._native = ftype(buf) def __call__(self, *args): return self._native(*args) def __del__(self): if self._buf is not None: VirtualFree(self._buf) self._buf = None if struct.calcsize("P") == 4: CPUID0_INSNS = ( b"\x53" # push %ebx b"\x31\xc0" # xor %eax,%eax b"\x0f\xa2" # cpuid b"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax b"\x89\x18" # mov %ebx,0x0(%eax) b"\x89\x50\x04" # mov %edx,0x4(%eax) b"\x89\x48\x08" # mov %ecx,0x8(%eax) b"\x5b" # pop %ebx b"\xc3" # ret ) CPUID1_INSNS = ( b"\x53" # push %ebx b"\x31\xc0" # xor %eax,%eax b"\x40" # inc %eax b"\x0f\xa2" # cpuid b"\x5b" # pop %ebx b"\xc3" # ret ) else: CPUID0_INSNS = ( b"\x49\x89\xd8" # mov %rbx,%r8 b"\x49\x89\xc9" # mov %rcx,%r9 b"\x48\x31\xc0" # xor %rax,%rax b"\x0f\xa2" # cpuid b"\x4c\x89\xc8" # mov %r9,%rax b"\x89\x18" # mov %ebx,0x0(%rax) b"\x89\x50\x04" # mov %edx,0x4(%rax) b"\x89\x48\x08" # mov %ecx,0x8(%rax) b"\x4c\x89\xc3" # mov %r8,%rbx b"\xc3" # retq ) CPUID1_INSNS = ( b"\x53" # push %rbx b"\x48\x31\xc0" # xor %rax,%rax b"\x48\xff\xc0" # inc %rax b"\x0f\xa2" # cpuid b"\x5b" # pop %rbx b"\xc3" # retq ) def cpuid0(): _cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS) buf = create_string_buffer(12) def cpuid0(): _cpuid0(buf) return buf.raw return cpuid0 cpuid0 = cpuid0() cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS) class DataBlob(Structure): _fields_ = [('cbData', c_uint), ('pbData', c_void_p)] DataBlob_p = POINTER(DataBlob) def CryptUnprotectData(): _CryptUnprotectData = crypt32.CryptUnprotectData _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, c_void_p, c_void_p, c_uint, DataBlob_p] _CryptUnprotectData.restype = c_uint def CryptUnprotectData(indata, entropy): indatab = create_string_buffer(indata) indata = DataBlob(len(indata), cast(indatab, c_void_p)) entropyb = create_string_buffer(entropy) entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) outdata = DataBlob() if not _CryptUnprotectData(byref(indata), None, byref(entropy), None, None, 0, byref(outdata)): raise ADEPTError("Failed to decrypt user key key (sic)") return string_at(outdata.pbData, outdata.cbData) return CryptUnprotectData CryptUnprotectData = CryptUnprotectData() def adeptkeys(): if AES is None: raise ADEPTError("PyCrypto or OpenSSL must be installed") root = GetSystemDirectory().split('\\')[0] + '\\' serial = GetVolumeSerialNumber(root) vendor = cpuid0() signature = struct.pack('>I', cpuid1())[1:] user = GetUserName() entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) cuser = winreg.HKEY_CURRENT_USER try: regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH) device = winreg.QueryValueEx(regkey, 'key')[0] except WindowsError: raise ADEPTError("Adobe Digital Editions not activated") keykey = CryptUnprotectData(device, entropy) userkey = None keys = [] try: plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) except WindowsError: raise ADEPTError("Could not locate ADE activation") for i in range(0, 16): try: plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) except WindowsError: break ktype = winreg.QueryValueEx(plkparent, None)[0] if ktype != 'credentials': continue for j in range(0, 16): try: plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) except WindowsError: break ktype = winreg.QueryValueEx(plkkey, None)[0] if ktype != 'privateLicenseKey': continue userkey = winreg.QueryValueEx(plkkey, 'value')[0] userkey = b64decode(userkey) aes = AES(keykey) userkey = aes.decrypt(userkey) userkey = userkey[26:-ord(userkey[-1:])] #print "found key:",userkey.encode('hex') keys.append(userkey) if len(keys) == 0: raise ADEPTError('Could not locate privateLicenseKey') print("Found {0:d} keys".format(len(keys))) return keys elif isosx: import xml.etree.ElementTree as etree import subprocess NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} def findActivationDat(): import warnings warnings.filterwarnings('ignore', category=FutureWarning) home = os.getenv('HOME') cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"' cmdline = cmdline.encode(sys.getfilesystemencoding()) p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) out1, out2 = p2.communicate() reslst = out1.split(b'\n') cnt = len(reslst) ActDatPath = b"activation.dat" for j in range(cnt): resline = reslst[j] pp = resline.find(b'activation.dat') if pp >= 0: ActDatPath = resline break if os.path.exists(ActDatPath): return ActDatPath return None def adeptkeys(): actpath = findActivationDat() if actpath is None: raise ADEPTError("Could not find ADE activation.dat file.") tree = etree.parse(actpath) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) userkey = tree.findtext(expr) userkey = b64decode(userkey) userkey = userkey[26:] return [userkey] else: def adeptkeys(): raise ADEPTError("This script only supports Windows and Mac OS X.") return [] # interface for Python DeDRM def getkey(outpath): keys = adeptkeys() if len(keys) > 0: if not os.path.isdir(outpath): outfile = outpath with open(outfile, 'wb') as keyfileout: keyfileout.write(keys[0]) print("Saved a key to {0}".format(outfile)) else: keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'wb') as keyfileout: keyfileout.write(key) print("Saved a key to {0}".format(outfile)) return True return False def usage(progname): print("Finds, decrypts and saves the default Adobe Adept encryption key(s).") print("Keys are saved to the current directory, or a specified output directory.") print("If a file name is passed instead of a directory, only the first key is saved, in that file.") print("Usage:") print(" {0:s} [-h] []".format(progname)) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) print("{0} v{1}\nCopyright © 2009-2020 i♥cabbages, Apprentice Harper et al.".format(progname,__version__)) try: opts, args = getopt.getopt(argv[1:], "h") except getopt.GetoptError as err: print("Error in options or arguments: {0}".format(err.args[0])) usage(progname) sys.exit(2) for o, a in opts: if o == "-h": usage(progname) sys.exit(0) if len(args) > 1: usage(progname) sys.exit(2) if len(args) == 1: # save to the specified file or directory outpath = args[0] if not os.path.isabs(outpath): outpath = os.path.abspath(outpath) else: # save to the same directory as the script outpath = os.path.dirname(argv[0]) # make sure the outpath is the outpath = os.path.realpath(os.path.normpath(outpath)) keys = adeptkeys() if len(keys) > 0: if not os.path.isdir(outpath): outfile = outpath with open(outfile, 'wb') as keyfileout: keyfileout.write(keys[0]) print("Saved a key to {0}".format(outfile)) else: keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'wb') as keyfileout: keyfileout.write(key) print("Saved a key to {0}".format(outfile)) else: print("Could not retrieve Adobe Adept key.") return 0 def gui_main(): try: import tkinter import tkinter.constants import tkinter.messagebox import traceback except: return cli_main() class ExceptionDialog(tkinter.Frame): def __init__(self, root, text): tkinter.Frame.__init__(self, root, border=5) label = tkinter.Label(self, text="Unexpected error:", anchor=tkinter.constants.W, justify=tkinter.constants.LEFT) label.pack(fill=tkinter.constants.X, expand=0) self.text = tkinter.Text(self) self.text.pack(fill=tkinter.constants.BOTH, expand=1) self.text.insert(tkinter.constants.END, text) argv=unicode_argv() root = tkinter.Tk() root.withdraw() progpath, progname = os.path.split(argv[0]) success = False try: keys = adeptkeys() keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(progpath,"adobekey_{0:d}.der".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'wb') as keyfileout: keyfileout.write(key) success = True tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile)) except ADEPTError as e: tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e))) except Exception: root.wm_state('normal') root.title(progname) text = traceback.format_exc() ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1) root.mainloop() if not success: return 1 return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/aescbc.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Routines for doing AES CBC in one file Modified by some_updates to extract and combine only those parts needed for AES CBC into one simple to add python file Original Version Copyright (c) 2002 by Paul A. Lambert Under: CryptoPy Artisitic License Version 1.0 See the wonderful pure python package cryptopy-1.2.5 and read its LICENSE.txt for complete license details. Adjusted for Python 3, September 2020 """ class CryptoError(Exception): """ Base class for crypto exceptions """ def __init__(self,errorMessage='Error!'): self.message = errorMessage def __str__(self): return self.message class InitCryptoError(CryptoError): """ Crypto errors during algorithm initialization """ class BadKeySizeError(InitCryptoError): """ Bad key size error """ class EncryptError(CryptoError): """ Error in encryption processing """ class DecryptError(CryptoError): """ Error in decryption processing """ class DecryptNotBlockAlignedError(DecryptError): """ Error in decryption processing """ def xorS(a,b): """ XOR two strings """ assert len(a)==len(b) x = [] for i in range(len(a)): x.append( chr(ord(a[i])^ord(b[i]))) return ''.join(x) def xor(a,b): """ XOR two strings """ x = [] for i in range(min(len(a),len(b))): x.append( chr(ord(a[i])^ord(b[i]))) return ''.join(x) """ Base 'BlockCipher' and Pad classes for cipher instances. BlockCipher supports automatic padding and type conversion. The BlockCipher class was written to make the actual algorithm code more readable and not for performance. """ class BlockCipher: """ Block ciphers """ def __init__(self): self.reset() def reset(self): self.resetEncrypt() self.resetDecrypt() def resetEncrypt(self): self.encryptBlockCount = 0 self.bytesToEncrypt = '' def resetDecrypt(self): self.decryptBlockCount = 0 self.bytesToDecrypt = '' def encrypt(self, plainText, more = None): """ Encrypt a string and return a binary string """ self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize) cipherText = '' for i in range(numBlocks): bStart = i*self.blockSize ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize]) self.encryptBlockCount += 1 cipherText += ctBlock if numExtraBytes > 0: # save any bytes that are not block aligned self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] else: self.bytesToEncrypt = '' if more == None: # no more data expected from caller finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) if len(finalBytes) > 0: ctBlock = self.encryptBlock(finalBytes) self.encryptBlockCount += 1 cipherText += ctBlock self.resetEncrypt() return cipherText def decrypt(self, cipherText, more = None): """ Decrypt a string and return a string """ self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) if more == None: # no more calls to decrypt, should have all the data if numExtraBytes != 0: raise DecryptNotBlockAlignedError('Data not block aligned on decrypt') # hold back some bytes in case last decrypt has zero len if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : numBlocks -= 1 numExtraBytes = self.blockSize plainText = '' for i in range(numBlocks): bStart = i*self.blockSize ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) self.decryptBlockCount += 1 plainText += ptBlock if numExtraBytes > 0: # save any bytes that are not block aligned self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] else: self.bytesToEncrypt = '' if more == None: # last decrypt remove padding plainText = self.padding.removePad(plainText, self.blockSize) self.resetDecrypt() return plainText class Pad: def __init__(self): pass # eventually could put in calculation of min and max size extension class padWithPadLen(Pad): """ Pad a binary string with the length of the padding """ def addPad(self, extraBytes, blockSize): """ Add padding to a binary string to make it an even multiple of the block size """ blocks, numExtraBytes = divmod(len(extraBytes), blockSize) padLength = blockSize - numExtraBytes return extraBytes + padLength*chr(padLength) def removePad(self, paddedBinaryString, blockSize): """ Remove padding from a binary string """ if not(0 6 and i%Nk == 4 : temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) return w Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) #------------------------------------- def AddRoundKey(algInstance, keyBlock): """ XOR the algorithm state with a block of key material """ for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] ^= keyBlock[column][row] #------------------------------------- def SubBytes(algInstance): for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] = Sbox[algInstance.state[column][row]] def InvSubBytes(algInstance): for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) #------------------------------------- """ For each block size (Nb), the ShiftRow operation shifts row i by the amount Ci. Note that row 0 is not shifted. Nb C1 C2 C3 ------------------- """ shiftOffset = { 4 : ( 0, 1, 2, 3), 5 : ( 0, 1, 2, 3), 6 : ( 0, 1, 2, 3), 7 : ( 0, 1, 2, 4), 8 : ( 0, 1, 3, 4) } def ShiftRows(algInstance): tmp = [0]*algInstance.Nb # list of size Nb for r in range(1,4): # row 0 reamains unchanged and can be skipped for c in range(algInstance.Nb): tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] for c in range(algInstance.Nb): algInstance.state[c][r] = tmp[c] def InvShiftRows(algInstance): tmp = [0]*algInstance.Nb # list of size Nb for r in range(1,4): # row 0 reamains unchanged and can be skipped for c in range(algInstance.Nb): tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] for c in range(algInstance.Nb): algInstance.state[c][r] = tmp[c] #------------------------------------- def MixColumns(a): Sprime = [0,0,0,0] for j in range(a.Nb): # for each column Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) for i in range(4): a.state[j][i] = Sprime[i] def InvMixColumns(a): """ Mix the four bytes of every column in a linear way This is the opposite operation of Mixcolumn """ Sprime = [0,0,0,0] for j in range(a.Nb): # for each column Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) for i in range(4): a.state[j][i] = Sprime[i] #------------------------------------- def mul(a, b): """ Multiply two elements of GF(2^m) needed for MixColumn and InvMixColumn """ if (a !=0 and b!=0): return Alogtable[(Logtable[a] + Logtable[b])%255] else: return 0 Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) """ AES Encryption Algorithm The AES algorithm is just Rijndael algorithm restricted to the default blockSize of 128 bits. """ class AES(Rijndael): """ The AES algorithm is the Rijndael block cipher restricted to block sizes of 128 bits and key sizes of 128, 192 or 256 bits """ def __init__(self, key = None, padding = padWithPadLen(), keySize=16): """ Initialize AES, keySize is in bytes """ if not (keySize == 16 or keySize == 24 or keySize == 32) : raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes') Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) self.name = 'AES' """ CBC mode of encryption for block ciphers. This algorithm mode wraps any BlockCipher to make a Cipher Block Chaining mode. """ from random import Random # should change to crypto.random!!! class CBC(BlockCipher): """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode algorithms. The initialization (IV) is automatic if set to None. Padding is also automatic based on the Pad class used to initialize the algorithm """ def __init__(self, blockCipherInstance, padding = padWithPadLen()): """ CBC algorithms are created by initializing with a BlockCipher instance """ self.baseCipher = blockCipherInstance self.name = self.baseCipher.name + '_CBC' self.blockSize = self.baseCipher.blockSize self.keySize = self.baseCipher.keySize self.padding = padding self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! self.r = Random() # for IV generation, currently uses # mediocre standard distro version <---------------- import time newSeed = time.ctime()+str(self.r) # seed with instance location self.r.seed(newSeed) # to make unique self.reset() def setKey(self, key): self.baseCipher.setKey(key) # Overload to reset both CBC state and the wrapped baseCipher def resetEncrypt(self): BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) self.baseCipher.resetEncrypt() # reset base cipher encrypt state def resetDecrypt(self): BlockCipher.resetDecrypt(self) # reset CBC state (super class) self.baseCipher.resetDecrypt() # reset base cipher decrypt state def encrypt(self, plainText, iv=None, more=None): """ CBC encryption - overloads baseCipher to allow optional explicit IV when iv=None, iv is auto generated! """ if self.encryptBlockCount == 0: self.iv = iv else: assert(iv==None), 'IV used only on first call to encrypt' return BlockCipher.encrypt(self,plainText, more=more) def decrypt(self, cipherText, iv=None, more=None): """ CBC decryption - overloads baseCipher to allow optional explicit IV when iv=None, iv is auto generated! """ if self.decryptBlockCount == 0: self.iv = iv else: assert(iv==None), 'IV used only on first call to decrypt' return BlockCipher.decrypt(self, cipherText, more=more) def encryptBlock(self, plainTextBlock): """ CBC block encryption, IV is set with 'encrypt' """ auto_IV = '' if self.encryptBlockCount == 0: if self.iv == None: # generate IV and use self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) self.prior_encr_CT_block = self.iv auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic else: # application provided IV assert(len(self.iv) == self.blockSize ),'IV must be same length as block' self.prior_encr_CT_block = self.iv """ encrypt the prior CT XORed with the PT """ ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) self.prior_encr_CT_block = ct return auto_IV+ct def decryptBlock(self, encryptedBlock): """ Decrypt a single block """ if self.decryptBlockCount == 0: # first call, process IV if self.iv == None: # auto decrypt IV? self.prior_CT_block = encryptedBlock return '' else: assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" self.prior_CT_block = self.iv dct = self.baseCipher.decryptBlock(encryptedBlock) """ XOR the prior decrypted CT with the prior CT """ dct_XOR_priorCT = xor( self.prior_CT_block, dct ) self.prior_CT_block = encryptedBlock return dct_XOR_priorCT """ AES_CBC Encryption Algorithm """ class AES_CBC(CBC): """ AES encryption in CBC feedback mode """ def __init__(self, key=None, padding=padWithPadLen(), keySize=16): CBC.__init__( self, AES(key, noPadding(), keySize), padding) self.name = 'AES_CBC' ================================================ FILE: DeDRM_plugin/alfcrypto.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # crypto library mainly by some_updates # pbkdf2.py pbkdf2 code taken from pbkdf2.py # pbkdf2.py Copyright © 2004 Matt Johnston # pbkdf2.py Copyright © 2009 Daniel Holth # pbkdf2.py This code may be freely used and modified for any purpose. import sys, os import hmac from struct import pack import hashlib # interface to needed routines libalfcrypto def _load_libalfcrypto(): import ctypes from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof pointer_size = ctypes.sizeof(ctypes.c_voidp) name_of_lib = None if sys.platform.startswith('darwin'): name_of_lib = 'libalfcrypto.dylib' elif sys.platform.startswith('win'): if pointer_size == 4: name_of_lib = 'alfcrypto.dll' else: name_of_lib = 'alfcrypto64.dll' else: if pointer_size == 4: name_of_lib = 'libalfcrypto32.so' else: name_of_lib = 'libalfcrypto64.so' # hard code to local location for libalfcrypto libalfcrypto = os.path.join(sys.path[0],name_of_lib) if not os.path.isfile(libalfcrypto): libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib) if not os.path.isfile(libalfcrypto): libalfcrypto = os.path.join('.',name_of_lib) if not os.path.isfile(libalfcrypto): raise Exception('libalfcrypto not found at %s' % libalfcrypto) libalfcrypto = CDLL(libalfcrypto) c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) def F(restype, name, argtypes): func = getattr(libalfcrypto, name) func.restype = restype func.argtypes = argtypes return func # aes cbc decryption # # struct aes_key_st { # unsigned long rd_key[4 *(AES_MAXNR + 1)]; # int rounds; # }; # # typedef struct aes_key_st AES_KEY; # # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); # # # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, # const unsigned long length, const AES_KEY *key, # unsigned char *ivec, const int enc); AES_MAXNR = 14 class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) 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]) # Pukall 1 Cipher # unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src, # unsigned char *dest, unsigned int len, int decryption); PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong]) # Topaz Encryption # typedef struct _TpzCtx { # unsigned int v[2]; # } TpzCtx; # # void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen); # void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len); class TPZ_CTX(Structure): _fields_ = [('v', c_long * 2)] TPZ_CTX_p = POINTER(TPZ_CTX) topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong]) topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong]) class AES_CBC(object): def __init__(self): self._blocksize = 0 self._keyctx = None self._iv = 0 def set_decrypt_key(self, userkey, iv): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise Exception('AES CBC improper key used') return keyctx = self._keyctx = AES_KEY() self._iv = iv rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: raise Exception('Failed to initialize AES CBC key') def decrypt(self, data): out = create_string_buffer(len(data)) mutable_iv = create_string_buffer(self._iv, len(self._iv)) rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0) if rv == 0: raise Exception('AES CBC decryption failed') return out.raw class Pukall_Cipher(object): def __init__(self): self.key = None def PC1(self, key, src, decryption=True): self.key = key out = create_string_buffer(len(src)) de = 0 if decryption: de = 1 rv = PC1(key, len(key), src, out, len(src), de) return out.raw class Topaz_Cipher(object): def __init__(self): self._ctx = None def ctx_init(self, key): tpz_ctx = self._ctx = TPZ_CTX() topazCryptoInit(tpz_ctx, key, len(key)) return tpz_ctx def decrypt(self, data, ctx=None): if ctx == None: ctx = self._ctx out = create_string_buffer(len(data)) topazCryptoDecrypt(ctx, data, out, len(data)) return out.raw print("Using Library AlfCrypto DLL/DYLIB/SO") return (AES_CBC, Pukall_Cipher, Topaz_Cipher) def _load_python_alfcrypto(): import aescbc class Pukall_Cipher(object): def __init__(self): self.key = None def PC1(self, key, src, decryption=True): sum1 = 0; sum2 = 0; keyXorVal = 0; if len(key)!=16: raise Exception('Pukall_Cipher: Bad key length.') wkey = [] for i in range(8): wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) dst = "" for i in range(len(src)): temp1 = 0; byteXorVal = 0; for j in range(8): temp1 ^= wkey[j] sum2 = (sum2+j)*20021 + sum1 sum1 = (temp1*346)&0xFFFF sum2 = (sum2+sum1)&0xFFFF temp1 = (temp1*20021+1)&0xFFFF byteXorVal ^= temp1 ^ sum2 curByte = ord(src[i]) if not decryption: keyXorVal = curByte * 257; curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF if decryption: keyXorVal = curByte * 257; for j in range(8): wkey[j] ^= keyXorVal; dst+=chr(curByte) return dst class Topaz_Cipher(object): def __init__(self): self._ctx = None def ctx_init(self, key): ctx1 = 0x0CAFFE19E for keyChar in key: keyByte = ord(keyChar) ctx2 = ctx1 ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) self._ctx = [ctx1, ctx2] return [ctx1,ctx2] def decrypt(self, data, ctx=None): if ctx == None: ctx = self._ctx ctx1 = ctx[0] ctx2 = ctx[1] plainText = "" for dataChar in data: dataByte = ord(dataChar) m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF ctx2 = ctx1 ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) plainText += chr(m) return plainText class AES_CBC(object): def __init__(self): self._key = None self._iv = None self.aes = None def set_decrypt_key(self, userkey, iv): self._key = userkey self._iv = iv self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey)) def decrypt(self, data): iv = self._iv cleartext = self.aes.decrypt(iv + data) return cleartext print("Using Library AlfCrypto Python") return (AES_CBC, Pukall_Cipher, Topaz_Cipher) def _load_crypto(): AES_CBC = Pukall_Cipher = Topaz_Cipher = None cryptolist = (_load_libalfcrypto, _load_python_alfcrypto) for loader in cryptolist: try: AES_CBC, Pukall_Cipher, Topaz_Cipher = loader() break except (ImportError, Exception): pass return AES_CBC, Pukall_Cipher, Topaz_Cipher AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto() class KeyIVGen(object): # this only exists in openssl so we will use pure python implementation instead # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) def pbkdf2(self, passwd, salt, iter, keylen): def xorbytes( a, b ): if len(a) != len(b): raise Exception("xorbytes(): lengths differ") return bytes([x ^ y for x, y in zip(a, b)]) def prf( h, data ): hm = h.copy() hm.update( data ) return hm.digest() def pbkdf2_F( h, salt, itercount, blocknum ): U = prf( h, salt + pack('>i',blocknum ) ) T = U for i in range(2, itercount+1): U = prf( h, U ) T = xorbytes( T, U ) return T sha = hashlib.sha1 digest_size = sha().digest_size # l - number of output blocks to produce l = keylen // digest_size if keylen % digest_size != 0: l += 1 h = hmac.new( passwd, None, sha ) T = b"" for i in range(1, l+1): T += pbkdf2_F( h, salt, iter, i ) return T[0: keylen] ================================================ FILE: DeDRM_plugin/androidkindlekey.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # androidkindlekey.py # Copyright © 2010-20 by Thom, Apprentice Harper et al. # Revision history: # 1.0 - AmazonSecureStorage.xml decryption to serial number # 1.1 - map_data_storage.db decryption to serial number # 1.2 - Changed to be callable from AppleScript by returning only serial number # - and changed name to androidkindlekey.py # - and added in unicode command line support # 1.3 - added in TkInter interface, output to a file # 1.4 - Fix some problems identified by Aldo Bleeker # 1.5 - Fix another problem identified by Aldo Bleeker # 2.0 - Python 3 compatibility """ Retrieve Kindle for Android Serial Number. """ __license__ = 'GPL v3' __version__ = '2.0' import os import sys import traceback import getopt import tempfile import zlib import tarfile from hashlib import md5 from io import BytesIO from binascii import a2b_hex, b2a_hex # Routines common to Mac and PC # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data,str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["kindlekey.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class DrmException(Exception): pass STORAGE = "backup.ab" STORAGE1 = "AmazonSecureStorage.xml" STORAGE2 = "map_data_storage.db" class AndroidObfuscation(object): '''AndroidObfuscation For the key, it's written in java, and run in android dalvikvm ''' key = a2b_hex('0176e04c9408b1702d90be333fd53523') def encrypt(self, plaintext): cipher = self._get_cipher() padding = len(self.key) - len(plaintext) % len(self.key) plaintext += chr(padding) * padding return b2a_hex(cipher.encrypt(plaintext.encode('utf-8'))) def decrypt(self, ciphertext): cipher = self._get_cipher() plaintext = cipher.decrypt(a2b_hex(ciphertext)) return plaintext[:-ord(plaintext[-1])] def _get_cipher(self): try: from Crypto.Cipher import AES return AES.new(self.key) except ImportError: from aescbc import AES, noPadding return AES(self.key, padding=noPadding()) class AndroidObfuscationV2(AndroidObfuscation): '''AndroidObfuscationV2 ''' count = 503 password = b'Thomsun was here!' def __init__(self, salt): key = self.password + salt for _ in range(self.count): key = md5(key).digest() self.key = key[:8] self.iv = key[8:16] def _get_cipher(self): try : from Crypto.Cipher import DES return DES.new(self.key, DES.MODE_CBC, self.iv) except ImportError: from python_des import Des, CBC return Des(self.key, CBC, self.iv) def parse_preference(path): ''' parse android's shared preference xml ''' storage = {} read = open(path) for line in read: line = line.strip() # value if line.startswith(' 0: dsns.append(device_data_row[0]) except: print("Error getting one of the device serial name keys") traceback.print_exc() pass dsns = list(set(dsns)) cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') userdata_keys = cursor.fetchall() tokens = [] for userdata_row in userdata_keys: try: if userdata_row and userdata_row[0]: if len(userdata_row[0]) > 0: if ',' in userdata_row[0]: splits = userdata_row[0].split(',') for split in splits: tokens.append(split) tokens.append(userdata_row[0]) except: print("Error getting one of the account token keys") traceback.print_exc() pass tokens = list(set(tokens)) serials = [] for x in dsns: serials.append(x) for y in tokens: serials.append(y) serials.append(x+y) return serials def get_serials(path=STORAGE): '''get serials from files in from android backup.ab backup.ab can be get using adb command: shell> adb backup com.amazon.kindle or from individual files if they're passed. ''' if not os.path.isfile(path): return [] basename = os.path.basename(path) if basename == STORAGE1: return get_serials1(path) elif basename == STORAGE2: return get_serials2(path) output = None try : read = open(path, 'rb') head = read.read(24) if head[:14] == b'ANDROID BACKUP': output = BytesIO(zlib.decompress(read.read())) except Exception: pass finally: read.close() if not output: return [] serials = [] tar = tarfile.open(fileobj=output) for member in tar.getmembers(): if member.name.strip().endswith(STORAGE1): write = tempfile.NamedTemporaryFile(mode='wb', delete=False) write.write(tar.extractfile(member).read()) write.close() write_path = os.path.abspath(write.name) serials.extend(get_serials1(write_path)) os.remove(write_path) elif member.name.strip().endswith(STORAGE2): write = tempfile.NamedTemporaryFile(mode='wb', delete=False) write.write(tar.extractfile(member).read()) write.close() write_path = os.path.abspath(write.name) serials.extend(get_serials2(write_path)) os.remove(write_path) return list(set(serials)) __all__ = [ 'get_serials', 'getkey'] # procedure for CLI and GUI interfaces # returns single or multiple keys (one per line) in the specified file def getkey(outfile, inpath): keys = get_serials(inpath) if len(keys) > 0: with open(outfile, 'w') as keyfileout: for key in keys: keyfileout.write(key) keyfileout.write("\n") return True return False def usage(progname): print("Decrypts the serial number(s) of Kindle For Android from Android backup or file") print("Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+.") print("Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml") print("Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db") print("") print("Usage:") print(" {0:s} [-h] [-b ] []".format(progname)) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__)) try: opts, args = getopt.getopt(argv[1:], "hb:") except getopt.GetoptError as err: usage(progname) print("\nError in options or arguments: {0}".format(err.args[0])) return 2 inpath = "" for o, a in opts: if o == "-h": usage(progname) return 0 if o == "-b": inpath = a if len(args) > 1: usage(progname) return 2 if len(args) == 1: # save to the specified file or directory outfile = args[0] if not os.path.isabs(outfile): outfile = os.path.join(os.path.dirname(argv[0]),outfile) outfile = os.path.abspath(outfile) if os.path.isdir(outfile): outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") else: # save to the same directory as the script outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a") # make sure the outpath is OK outfile = os.path.realpath(os.path.normpath(outfile)) if not os.path.isfile(inpath): usage(progname) print("\n{0:s} file not found".format(inpath)) return 2 if getkey(outfile, inpath): print("\nSaved Kindle for Android key to {0}".format(outfile)) else: print("\nCould not retrieve Kindle for Android key.") return 0 def gui_main(): try: import tkinter import tkinter.constants import tkinter.messagebox import tkinter.filedialog except: print("tkinter not installed") return 0 class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Select backup.ab file") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Backup file").grid(row=0, column=0) self.keypath = tkinter.Entry(body, width=40) self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.insert(2, "backup.ab") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) buttons = tkinter.Frame(self) buttons.pack() button2 = tkinter.Button( buttons, text="Extract", width=10, command=self.generate) button2.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button3 = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button3.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.askopenfilename( parent=None, title="Select backup.ab file", defaultextension=".ab", filetypes=[('adb backup com.amazon.kindle', '.ab'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def generate(self): inpath = self.keypath.get() self.status['text'] = "Getting key..." try: keys = get_serials(inpath) keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(progpath,"kindlekey{0:d}.k4a".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'w') as keyfileout: keyfileout.write(key) success = True tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile)) except Exception as e: self.status['text'] = "Error: {0}".format(e.args[0]) return self.status['text'] = "Select backup.ab file" argv=unicode_argv() progpath, progname = os.path.split(argv[0]) root = tkinter.Tk() root.title("Kindle for Android Key Extraction v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/argv_utils.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys, os import locale import codecs import importlib # get sys.argv arguments and encode them into utf-8 def unicode_argv(): if sys.platform.startswith('win'): # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["DeDRM.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] def add_cp65001_codec(): try: codecs.lookup('cp65001') except LookupError: codecs.register( lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) return def set_utf8_default_encoding(): if sys.getdefaultencoding() == 'utf-8': return # Regenerate setdefaultencoding. importlib.reload(sys) sys.setdefaultencoding('utf-8') for attr in dir(locale): if attr[0:3] != 'LC_': continue aref = getattr(locale, attr) try: locale.setlocale(aref, '') except locale.Error: continue try: lang = locale.getlocale(aref)[0] except (TypeError, ValueError): continue if lang: try: locale.setlocale(aref, (lang, 'UTF-8')) except locale.Error: os.environ[attr] = lang + '.UTF-8' try: locale.setlocale(locale.LC_ALL, '') except locale.Error: pass return ================================================ FILE: DeDRM_plugin/askfolder_ed.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # to work around tk_chooseDirectory not properly returning unicode paths on Windows # need to use a dialog that can be hacked up to actually return full unicode paths # originally based on AskFolder from EasyDialogs for Windows but modified to fix it # to actually use unicode for path # The original license for EasyDialogs is as follows # # Copyright (c) 2003-2005 Jimmy Retzlaff # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # Adjusted for Python 3, September 2020 """ AskFolder(...) -- Ask the user to select a folder Windows specific """ import os import ctypes from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR import ctypes.wintypes as wintypes __all__ = ['AskFolder'] # Load required Windows DLLs ole32 = ctypes.windll.ole32 shell32 = ctypes.windll.shell32 user32 = ctypes.windll.user32 # Windows Constants BFFM_INITIALIZED = 1 BFFM_SETOKTEXT = 1129 BFFM_SETSELECTIONA = 1126 BFFM_SETSELECTIONW = 1127 BIF_EDITBOX = 16 BS_DEFPUSHBUTTON = 1 CB_ADDSTRING = 323 CB_GETCURSEL = 327 CB_SETCURSEL = 334 CDM_SETCONTROLTEXT = 1128 EM_GETLINECOUNT = 186 EM_GETMARGINS = 212 EM_POSFROMCHAR = 214 EM_SETSEL = 177 GWL_STYLE = -16 IDC_STATIC = -1 IDCANCEL = 2 IDNO = 7 IDOK = 1 IDYES = 6 MAX_PATH = 260 OFN_ALLOWMULTISELECT = 512 OFN_ENABLEHOOK = 32 OFN_ENABLESIZING = 8388608 OFN_ENABLETEMPLATEHANDLE = 128 OFN_EXPLORER = 524288 OFN_OVERWRITEPROMPT = 2 OPENFILENAME_SIZE_VERSION_400 = 76 PBM_GETPOS = 1032 PBM_SETMARQUEE = 1034 PBM_SETPOS = 1026 PBM_SETRANGE = 1025 PBM_SETRANGE32 = 1030 PBS_MARQUEE = 8 PM_REMOVE = 1 SW_HIDE = 0 SW_SHOW = 5 SW_SHOWNORMAL = 1 SWP_NOACTIVATE = 16 SWP_NOMOVE = 2 SWP_NOSIZE = 1 SWP_NOZORDER = 4 VER_PLATFORM_WIN32_NT = 2 WM_COMMAND = 273 WM_GETTEXT = 13 WM_GETTEXTLENGTH = 14 WM_INITDIALOG = 272 WM_NOTIFY = 78 # Windows function prototypes BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM) # Windows types LPCTSTR = ctypes.c_char_p LPTSTR = ctypes.c_char_p LPVOID = ctypes.c_voidp TCHAR = ctypes.c_char class BROWSEINFO(ctypes.Structure): _fields_ = [ ("hwndOwner", wintypes.HWND), ("pidlRoot", LPVOID), ("pszDisplayName", LPTSTR), ("lpszTitle", LPCTSTR), ("ulFlags", ctypes.c_uint), ("lpfn", BrowseCallbackProc), ("lParam", wintypes.LPARAM), ("iImage", ctypes.c_int) ] # Utilities def CenterWindow(hwnd): desktopRect = GetWindowRect(user32.GetDesktopWindow()) myRect = GetWindowRect(hwnd) x = width(desktopRect) // 2 - width(myRect) // 2 y = height(desktopRect) // 2 - height(myRect) // 2 user32.SetWindowPos(hwnd, 0, desktopRect.left + x, desktopRect.top + y, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER ) def GetWindowRect(hwnd): rect = wintypes.RECT() user32.GetWindowRect(hwnd, ctypes.byref(rect)) return rect def width(rect): return rect.right-rect.left def height(rect): return rect.bottom-rect.top def AskFolder( message=None, version=None, defaultLocation=None, location=None, windowTitle=None, actionButtonLabel=None, cancelButtonLabel=None, multiple=None): """Display a dialog asking the user for select a folder. modified to use unicode strings as much as possible returns unicode path """ def BrowseCallback(hwnd, uMsg, lParam, lpData): if uMsg == BFFM_INITIALIZED: if actionButtonLabel: label = str(actionButtonLabel, errors='replace') user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label) if cancelButtonLabel: label = str(cancelButtonLabel, errors='replace') cancelButton = user32.GetDlgItem(hwnd, IDCANCEL) if cancelButton: user32.SetWindowTextW(cancelButton, label) if windowTitle: title = str(windowTitle, errors='replace') user32.SetWindowTextW(hwnd, title) if defaultLocation: user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\')) if location: x, y = location desktopRect = wintypes.RECT() user32.GetWindowRect(0, ctypes.byref(desktopRect)) user32.SetWindowPos(hwnd, 0, desktopRect.left + x, desktopRect.top + y, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER) else: CenterWindow(hwnd) return 0 # This next line is needed to prevent gc of the callback callback = BrowseCallbackProc(BrowseCallback) browseInfo = BROWSEINFO() browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1)) browseInfo.lpszTitle = message browseInfo.lpfn = callback pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo)) if not pidl: result = None else: path = LPCWSTR(" " * (MAX_PATH+1)) shell32.SHGetPathFromIDListW(pidl, path) ole32.CoTaskMemFree(pidl) result = path.value return result ================================================ FILE: DeDRM_plugin/config.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- __license__ = 'GPL v3' # Python 3, September 2020 # Standard Python modules. import os, traceback, json, codecs from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) from PyQt5 import Qt as QtGui from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx # modules from this plugin's zipfile. from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name from calibre_plugins.dedrm.utilities import uStrCmp import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.androidkindlekey as androidkindlekey class ConfigWidget(QWidget): def __init__(self, plugin_path, alfdir): QWidget.__init__(self) self.plugin_path = plugin_path self.alfdir = alfdir # get the prefs self.dedrmprefs = prefs.DeDRM_Prefs() # make a local copy self.tempdedrmprefs = {} self.tempdedrmprefs['bandnkeys'] = self.dedrmprefs['bandnkeys'].copy() self.tempdedrmprefs['adeptkeys'] = self.dedrmprefs['adeptkeys'].copy() self.tempdedrmprefs['ereaderkeys'] = self.dedrmprefs['ereaderkeys'].copy() self.tempdedrmprefs['kindlekeys'] = self.dedrmprefs['kindlekeys'].copy() self.tempdedrmprefs['androidkeys'] = self.dedrmprefs['androidkeys'].copy() self.tempdedrmprefs['pids'] = list(self.dedrmprefs['pids']) self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials']) self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix'] self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix'] # Start Qt Gui dialog layout layout = QVBoxLayout(self) self.setLayout(layout) help_layout = QHBoxLayout() layout.addLayout(help_layout) # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked. help_label = QLabel('Plugin Help', self) help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) help_label.setAlignment(Qt.AlignRight) help_label.linkActivated.connect(self.help_link_activated) help_layout.addWidget(help_label) keys_group_box = QGroupBox(_('Configuration:'), self) layout.addWidget(keys_group_box) keys_group_box_layout = QHBoxLayout() keys_group_box.setLayout(keys_group_box_layout) button_layout = QVBoxLayout() keys_group_box_layout.addLayout(button_layout) self.bandn_button = QtGui.QPushButton(self) self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks")) self.bandn_button.setText("Barnes and Noble ebooks") self.bandn_button.clicked.connect(self.bandn_keys) self.kindle_android_button = QtGui.QPushButton(self) self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks")) self.kindle_android_button.setText("Kindle for Android ebooks") self.kindle_android_button.clicked.connect(self.kindle_android) 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.setText("eInk Kindle ebooks") self.kindle_serial_button.clicked.connect(self.kindle_serials) 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.setText("Kindle for Mac/PC ebooks") self.kindle_key_button.clicked.connect(self.kindle_keys) self.adept_button = QtGui.QPushButton(self) self.adept_button.setToolTip(_("Click to manage keys for Adobe Digital Editions ebooks")) self.adept_button.setText("Adobe Digital Editions ebooks") self.adept_button.clicked.connect(self.adept_keys) self.mobi_button = QtGui.QPushButton(self) self.mobi_button.setToolTip(_("Click to manage PIDs for Mobipocket ebooks")) self.mobi_button.setText("Mobipocket ebooks") self.mobi_button.clicked.connect(self.mobi_keys) self.ereader_button = QtGui.QPushButton(self) self.ereader_button.setToolTip(_("Click to manage keys for eReader ebooks")) self.ereader_button.setText("eReader ebooks") self.ereader_button.clicked.connect(self.ereader_keys) button_layout.addWidget(self.kindle_serial_button) button_layout.addWidget(self.kindle_android_button) button_layout.addWidget(self.bandn_button) button_layout.addWidget(self.mobi_button) button_layout.addWidget(self.ereader_button) button_layout.addWidget(self.adept_button) button_layout.addWidget(self.kindle_key_button) self.resize(self.sizeHint()) def kindle_serials(self): d = ManageKeysDialog(self,"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog) d.exec_() def kindle_android(self): d = ManageKeysDialog(self,"Kindle for Android Key",self.tempdedrmprefs['androidkeys'], AddAndroidDialog, 'k4a') d.exec_() def kindle_keys(self): if isosx or iswindows: d = ManageKeysDialog(self,"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i') else: # linux d = ManageKeysDialog(self,"Kindle for Mac and PC Key",self.tempdedrmprefs['kindlekeys'], AddKindleDialog, 'k4i', self.tempdedrmprefs['kindlewineprefix']) d.exec_() self.tempdedrmprefs['kindlewineprefix'] = d.getwineprefix() def adept_keys(self): if isosx or iswindows: d = ManageKeysDialog(self,"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der') else: # linux d = ManageKeysDialog(self,"Adobe Digital Editions Key",self.tempdedrmprefs['adeptkeys'], AddAdeptDialog, 'der', self.tempdedrmprefs['adobewineprefix']) d.exec_() self.tempdedrmprefs['adobewineprefix'] = d.getwineprefix() def mobi_keys(self): d = ManageKeysDialog(self,"Mobipocket PID",self.tempdedrmprefs['pids'], AddPIDDialog) d.exec_() def bandn_keys(self): d = ManageKeysDialog(self,"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64') d.exec_() def ereader_keys(self): d = ManageKeysDialog(self,"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63') d.exec_() def help_link_activated(self, url): def get_help_file_resource(): # Copy the HTML helpfile to the plugin directory each time the # link is clicked in case the helpfile is updated in newer plugins. file_path = os.path.join(config_dir, "plugins", "DeDRM", "help", help_file_name) with open(file_path,'w') as f: f.write(self.load_resource(help_file_name)) return file_path url = 'file:///' + get_help_file_resource() open_url(QUrl(url)) def save_settings(self): self.dedrmprefs.set('bandnkeys', self.tempdedrmprefs['bandnkeys']) self.dedrmprefs.set('adeptkeys', self.tempdedrmprefs['adeptkeys']) self.dedrmprefs.set('ereaderkeys', self.tempdedrmprefs['ereaderkeys']) self.dedrmprefs.set('kindlekeys', self.tempdedrmprefs['kindlekeys']) self.dedrmprefs.set('androidkeys', self.tempdedrmprefs['androidkeys']) self.dedrmprefs.set('pids', self.tempdedrmprefs['pids']) self.dedrmprefs.set('serials', self.tempdedrmprefs['serials']) self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix']) self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix']) self.dedrmprefs.set('configured', True) self.dedrmprefs.writeprefs() def load_resource(self, name): with ZipFile(self.plugin_path, 'r') as zf: if name in zf.namelist(): return zf.read(name).decode('utf-8') return "" class ManageKeysDialog(QDialog): def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = "", wineprefix = None): QDialog.__init__(self,parent) self.parent = parent self.key_type_name = key_type_name self.plugin_keys = plugin_keys self.create_key = create_key self.keyfile_ext = keyfile_ext self.import_key = (keyfile_ext != "") self.binary_file = (keyfile_ext == "der") self.json_file = (keyfile_ext == "k4i") self.android_file = (keyfile_ext == "k4a") self.wineprefix = wineprefix self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name)) # Start Qt Gui dialog layout layout = QVBoxLayout(self) self.setLayout(layout) help_layout = QHBoxLayout() layout.addLayout(help_layout) # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked. help_label = QLabel('Help', self) help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) help_label.setAlignment(Qt.AlignRight) help_label.linkActivated.connect(self.help_link_activated) help_layout.addWidget(help_label) keys_group_box = QGroupBox(_("{0}s".format(self.key_type_name)), self) layout.addWidget(keys_group_box) keys_group_box_layout = QHBoxLayout() keys_group_box.setLayout(keys_group_box_layout) self.listy = QListWidget(self) self.listy.setToolTip("{0}s that will be used to decrypt ebooks".format(self.key_type_name)) self.listy.setSelectionMode(QAbstractItemView.SingleSelection) self.populate_list() keys_group_box_layout.addWidget(self.listy) button_layout = QVBoxLayout() keys_group_box_layout.addLayout(button_layout) self._add_key_button = QtGui.QToolButton(self) self._add_key_button.setIcon(QIcon(I('plus.png'))) self._add_key_button.setToolTip("Create new {0}".format(self.key_type_name)) self._add_key_button.clicked.connect(self.add_key) button_layout.addWidget(self._add_key_button) self._delete_key_button = QtGui.QToolButton(self) self._delete_key_button.setToolTip(_("Delete highlighted key")) self._delete_key_button.setIcon(QIcon(I('list_remove.png'))) self._delete_key_button.clicked.connect(self.delete_key) button_layout.addWidget(self._delete_key_button) if type(self.plugin_keys) == dict and self.import_key: self._rename_key_button = QtGui.QToolButton(self) self._rename_key_button.setToolTip(_("Rename highlighted key")) self._rename_key_button.setIcon(QIcon(I('edit-select-all.png'))) self._rename_key_button.clicked.connect(self.rename_key) button_layout.addWidget(self._rename_key_button) self.export_key_button = QtGui.QToolButton(self) self.export_key_button.setToolTip("Save highlighted key to a .{0} file".format(self.keyfile_ext)) self.export_key_button.setIcon(QIcon(I('save.png'))) self.export_key_button.clicked.connect(self.export_key) button_layout.addWidget(self.export_key_button) spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) button_layout.addItem(spacerItem) if self.wineprefix is not None: layout.addSpacing(5) wineprefix_layout = QHBoxLayout() layout.addLayout(wineprefix_layout) wineprefix_layout.setAlignment(Qt.AlignCenter) self.wp_label = QLabel("WINEPREFIX:") wineprefix_layout.addWidget(self.wp_label) self.wp_lineedit = QLineEdit(self) wineprefix_layout.addWidget(self.wp_lineedit) self.wp_label.setBuddy(self.wp_lineedit) self.wp_lineedit.setText(self.wineprefix) layout.addSpacing(5) migrate_layout = QHBoxLayout() layout.addLayout(migrate_layout) if self.import_key: migrate_layout.setAlignment(Qt.AlignJustify) self.migrate_btn = QPushButton("Import Existing Keyfiles", self) self.migrate_btn.setToolTip("Import *.{0} files (created using other tools).".format(self.keyfile_ext)) self.migrate_btn.clicked.connect(self.migrate_wrapper) migrate_layout.addWidget(self.migrate_btn) migrate_layout.addStretch() self.button_box = QDialogButtonBox(QDialogButtonBox.Close) self.button_box.rejected.connect(self.close) migrate_layout.addWidget(self.button_box) self.resize(self.sizeHint()) def getwineprefix(self): if self.wineprefix is not None: return str(self.wp_lineedit.text()).strip() return "" def populate_list(self): if type(self.plugin_keys) == dict: for key in self.plugin_keys.keys(): self.listy.addItem(QListWidgetItem(key)) else: for key in self.plugin_keys: self.listy.addItem(QListWidgetItem(key)) def add_key(self): d = self.create_key(self) d.exec_() if d.result() != d.Accepted: # New key generation cancelled. return new_key_value = d.key_value if type(self.plugin_keys) == dict: if new_key_value in self.plugin_keys.values(): old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0] info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), "The new {1} is the same as the existing {1} named {0} and has not been added.".format(old_key_name,self.key_type_name), show=True) return self.plugin_keys[d.key_name] = new_key_value else: if new_key_value in self.plugin_keys: info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), "This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True) return self.plugin_keys.append(d.key_value) self.listy.clear() self.populate_list() def rename_key(self): if not self.listy.currentItem(): errmsg = "No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name) r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return d = RenameKeyDialog(self) d.exec_() if d.result() != d.Accepted: # rename cancelled or moot. return keyname = str(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), "Do you really want to rename the {2} named {0} to {1}?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False): return self.plugin_keys[d.key_name] = self.plugin_keys[keyname] del self.plugin_keys[keyname] self.listy.clear() self.populate_list() def delete_key(self): if not self.listy.currentItem(): return keyname = str(self.listy.currentItem().text()) if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), "Do you really want to delete the {1} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: del self.plugin_keys[keyname] else: self.plugin_keys.remove(keyname) self.listy.clear() self.populate_list() def help_link_activated(self, url): def get_help_file_resource(): # Copy the HTML helpfile to the plugin directory each time the # link is clicked in case the helpfile is updated in newer plugins. help_file_name = "{0}_{1}_Help.htm".format(PLUGIN_NAME, self.key_type_name) file_path = os.path.join(config_dir, "plugins", "DeDRM", "help", help_file_name) with open(file_path,'w') as f: f.write(self.parent.load_resource(help_file_name)) return file_path url = 'file:///' + get_help_file_resource() open_url(QUrl(url)) def migrate_files(self): unique_dlg_name = PLUGIN_NAME + "import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory caption = "Select {0} files to import".format(self.key_type_name) filters = [("{0} files".format(self.key_type_name), [self.keyfile_ext])] files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: for filename in files: fpath = os.path.join(config_dir, filename) filename = os.path.basename(filename) new_key_name = os.path.splitext(os.path.basename(filename))[0] with open(fpath,'rb') as keyfile: new_key_value = keyfile.read() if self.binary_file: new_key_value = codecs.encode(new_key_value,'hex') elif self.json_file: new_key_value = json.loads(new_key_value) elif self.android_file: # convert to list of the keys in the string new_key_value = new_key_value.splitlines() match = False for key in self.plugin_keys.keys(): if uStrCmp(new_key_name, key, True): skipped += 1 msg = "A key with the name {0} already exists!\nSkipping key file {1}.\nRename the existing key and import again".format(new_key_name,filename) inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(msg), show_copy_button=False, show=True) match = True break if not match: if new_key_value in self.plugin_keys.values(): old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0] skipped += 1 info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), "The key in file {0} is the same as the existing key {1} and has been skipped.".format(filename,old_key_name), show_copy_button=False, show=True) else: counter += 1 self.plugin_keys[new_key_name] = new_key_value msg = "" if counter+skipped > 1: if counter > 0: msg += "Imported {0:d} key {1}. ".format(counter, "file" if counter == 1 else "files") if skipped > 0: msg += "Skipped {0:d} key {1}.".format(skipped, "file" if counter == 1 else "files") inf = info_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(msg), show_copy_button=False, show=True) return counter > 0 def migrate_wrapper(self): if self.migrate_files(): self.listy.clear() self.populate_list() def export_key(self): if not self.listy.currentItem(): errmsg = "No keyfile selected to export. Highlight a keyfile first." r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return keyname = str(self.listy.currentItem().text()) unique_dlg_name = PLUGIN_NAME + "export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory caption = "Save {0} File as...".format(self.key_type_name) filters = [("{0} Files".format(self.key_type_name), ["{0}".format(self.keyfile_ext)])] defaultname = "{0}.{1}".format(keyname, self.keyfile_ext) filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: if self.binary_file: with open(filename, 'wb') as fname: fname.write(codecs.decode(self.plugin_keys[keyname],'hex')) elif self.json_file: with open(filename, 'w') as fname: fname.write(json.dumps(self.plugin_keys[keyname])) elif self.android_file: with open(filename, 'w') as fname: for key in self.plugin_keys[keyname]: fname.write(key) fname.write('\n') else: with open(filename, 'w') as fname: fname.write(self.plugin_keys[keyname]) class RenameKeyDialog(QDialog): def __init__(self, parent=None,): print(repr(self), repr(parent)) QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name)) 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) data_group_box_layout.addWidget(QLabel('New Key Name:', self)) self.key_ledit = QLineEdit(self.parent.listy.currentItem().text(), self) self.key_ledit.setToolTip("Enter a new name for this existing {0}.".format(parent.key_type_name)) data_group_box_layout.addWidget(self.key_ledit) layout.addSpacing(20) 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()) def accept(self): if not str(self.key_ledit.text()) or str(self.key_ledit.text()).isspace(): errmsg = "Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) if len(self.key_ledit.text()) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) if uStrCmp(self.key_ledit.text(), self.parent.listy.currentItem().text()): # Same exact name ... do nothing. return QDialog.reject(self) for k in self.parent.plugin_keys.keys(): if (uStrCmp(self.key_ledit.text(), k, True) and not uStrCmp(k, self.parent.listy.currentItem().text(), True)): errmsg = "The key name {0} is already being used.".format(self.key_ledit.text()) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) QDialog.accept(self) @property def key_name(self): return str(self.key_ledit.text()).strip() class AddBandNKeyDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Create New Barnes & Noble Key".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("Unique Key Name:", self)) self.key_ledit = QLineEdit("", self) self.key_ledit.setToolTip(_("

Enter an identifying name for this new key.

" + "

It should be something that will help you remember " + "what personal information was used to create it.")) key_group.addWidget(self.key_ledit) name_group = QHBoxLayout() data_group_box_layout.addLayout(name_group) name_group.addWidget(QLabel("B&N/nook account email address:", self)) self.name_ledit = QLineEdit("", self) self.name_ledit.setToolTip(_("

Enter your email address as it appears in your B&N " + "account.

" + "

It will only be used to generate this " + "key and won\'t be stored anywhere " + "in calibre or on your computer.

" + "

eg: apprenticeharper@gmail.com

")) name_group.addWidget(self.name_ledit) name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self) name_disclaimer_label.setAlignment(Qt.AlignHCenter) data_group_box_layout.addWidget(name_disclaimer_label) ccn_group = QHBoxLayout() data_group_box_layout.addLayout(ccn_group) ccn_group.addWidget(QLabel("B&N/nook account password:", self)) self.cc_ledit = QLineEdit("", self) self.cc_ledit.setToolTip(_("

Enter the password " + "for your B&N account.

" + "

The password will only be used to generate this " + "key and won\'t be stored anywhere in " + "calibre or on your computer.")) ccn_group.addWidget(self.cc_ledit) ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) data_group_box_layout.addWidget(ccn_disclaimer_label) layout.addSpacing(10) key_group = QHBoxLayout() data_group_box_layout.addLayout(key_group) key_group.addWidget(QLabel("Retrieved key:", self)) self.key_display = QLabel("", self) self.key_display.setToolTip(_("Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers")) key_group.addWidget(self.key_display) self.retrieve_button = QtGui.QPushButton(self) self.retrieve_button.setToolTip(_("Click to retrieve your B&N encryption key from the B&N servers")) self.retrieve_button.setText("Retrieve Key") self.retrieve_button.clicked.connect(self.retrieve_key) key_group.addWidget(self.retrieve_button) layout.addSpacing(10) 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 str(self.key_ledit.text()).strip() @property def key_value(self): return str(self.key_display.text()).strip() @property def user_name(self): return str(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): return str(self.cc_ledit.text()).strip() def retrieve_key(self): from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key fetched_key = fetch_bandn_key(self.user_name,self.cc_number) if fetched_key == "": errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again." error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) else: self.key_display.setText(fetched_key) def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): errmsg = "All fields are required!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_value) == 0: self.retrieve_key() if len(self.key_value) == 0: return QDialog.accept(self) class AddEReaderDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Create New eReader Key".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("Unique Key Name:", self)) self.key_ledit = QLineEdit("", self) self.key_ledit.setToolTip("

Enter an identifying name for this new key.\nIt should be something that will help you remember what personal information was used to create it.") key_group.addWidget(self.key_ledit) name_group = QHBoxLayout() data_group_box_layout.addLayout(name_group) name_group.addWidget(QLabel("Your Name:", self)) self.name_ledit = QLineEdit("", self) self.name_ledit.setToolTip("Enter the name for this eReader key, usually the name on your credit card.\nIt will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.\n(ex: Mr Jonathan Q Smith)") name_group.addWidget(self.name_ledit) name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self) name_disclaimer_label.setAlignment(Qt.AlignHCenter) data_group_box_layout.addWidget(name_disclaimer_label) ccn_group = QHBoxLayout() data_group_box_layout.addLayout(ccn_group) ccn_group.addWidget(QLabel("Credit Card#:", self)) self.cc_ledit = QLineEdit("", self) self.cc_ledit.setToolTip("

Enter the last 8 digits of credit card number for this eReader key.\nThey will only be used to generate this one-time key and won\'t be stored anywhere in calibre or on your computer.") ccn_group.addWidget(self.cc_ledit) ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) ccn_disclaimer_label.setAlignment(Qt.AlignHCenter) data_group_box_layout.addWidget(ccn_disclaimer_label) layout.addSpacing(10) 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 str(self.key_ledit.text()).strip() @property def key_value(self): from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key return codecs.encode(generate_ereader_key(self.user_name, self.cc_number),'hex') @property def user_name(self): return str(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): return str(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): errmsg = "All fields are required!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if not self.cc_number.isdigit(): errmsg = "Numbers only in the credit card number field!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) class AddAdeptDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION)) layout = QVBoxLayout(self) self.setLayout(layout) try: if iswindows or isosx: from calibre_plugins.dedrm.adobekey import adeptkeys defaultkeys = adeptkeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(parent.parent.alfdir,"adobekey.py") defaultkeys = WineGetKeys(scriptpath, ".der",parent.getwineprefix()) self.default_key = defaultkeys[0] except: traceback.print_exc() self.default_key = "" self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) if len(self.default_key)>0: 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("Unique Key Name:", self)) self.key_ledit = QLineEdit("default_key", self) self.key_ledit.setToolTip("

Enter an identifying name for the current default Adobe Digital Editions key.") key_group.addWidget(self.key_ledit) self.button_box.accepted.connect(self.accept) else: default_key_error = QLabel("The default encryption key for Adobe Digital Editions could not be found.", self) default_key_error.setAlignment(Qt.AlignHCenter) layout.addWidget(default_key_error) # if no default, bot buttons do the same self.button_box.accepted.connect(self.reject) self.button_box.rejected.connect(self.reject) layout.addWidget(self.button_box) self.resize(self.sizeHint()) @property def key_name(self): return str(self.key_ledit.text()).strip() @property def key_value(self): return codecs.encode(self.default_key,'hex') def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): errmsg = "All fields are required!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) class AddKindleDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Getting Default Kindle for Mac/PC Key".format(PLUGIN_NAME, PLUGIN_VERSION)) layout = QVBoxLayout(self) self.setLayout(layout) try: if iswindows or isosx: from calibre_plugins.dedrm.kindlekey import kindlekeys defaultkeys = kindlekeys() else: # linux from .wineutils import WineGetKeys scriptpath = os.path.join(parent.parent.alfdir,"kindlekey.py") defaultkeys = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix()) self.default_key = defaultkeys[0] except: traceback.print_exc() self.default_key = "" self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) if len(self.default_key)>0: 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("Unique Key Name:", self)) self.key_ledit = QLineEdit("default_key", self) self.key_ledit.setToolTip("

Enter an identifying name for the current default Kindle for Mac/PC key.") key_group.addWidget(self.key_ledit) self.button_box.accepted.connect(self.accept) else: default_key_error = QLabel("The default encryption key for Kindle for Mac/PC could not be found.", self) default_key_error.setAlignment(Qt.AlignHCenter) layout.addWidget(default_key_error) # if no default, both buttons do the same self.button_box.accepted.connect(self.reject) self.button_box.rejected.connect(self.reject) layout.addWidget(self.button_box) self.resize(self.sizeHint()) @property def key_name(self): return str(self.key_ledit.text()).strip() @property def key_value(self): return self.default_key def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): errmsg = "All fields are required!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) class AddSerialDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Add New EInk Kindle Serial Number".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("EInk Kindle Serial Number:", self)) self.key_ledit = QLineEdit("", self) self.key_ledit.setToolTip("Enter an eInk Kindle serial number. EInk Kindle serial numbers are 16 characters long and usually start with a 'B' or a '9'. Kindle Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") 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 str(self.key_ledit.text()).strip() @property def key_value(self): return str(self.key_ledit.text()).replace(' ', '') def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): errmsg = "Please enter an eInk Kindle Serial Number or click Cancel in the dialog." return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) != 16: errmsg = "EInk Kindle Serial Numbers must be 16 characters long. This is {0:d} characters long.".format(len(self.key_name)) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) class AddAndroidDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Add new Kindle for Android Key".format(PLUGIN_NAME, PLUGIN_VERSION)) layout = QVBoxLayout(self) self.setLayout(layout) self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) data_group_box = QGroupBox("", self) layout.addWidget(data_group_box) data_group_box_layout = QVBoxLayout() data_group_box.setLayout(data_group_box_layout) file_group = QHBoxLayout() data_group_box_layout.addLayout(file_group) add_btn = QPushButton("Choose Backup File", self) add_btn.setToolTip("Import Kindle for Android backup file.") add_btn.clicked.connect(self.get_android_file) file_group.addWidget(add_btn) self.selected_file_name = QLabel("",self) self.selected_file_name.setAlignment(Qt.AlignHCenter) file_group.addWidget(self.selected_file_name) key_group = QHBoxLayout() data_group_box_layout.addLayout(key_group) key_group.addWidget(QLabel("Unique Key Name:", self)) self.key_ledit = QLineEdit("", self) self.key_ledit.setToolTip("

Enter an identifying name for the Android for Kindle key.") key_group.addWidget(self.key_ledit) #key_label = QLabel(_(''), self) #key_label.setAlignment(Qt.AlignHCenter) #data_group_box_layout.addWidget(key_label) 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 str(self.key_ledit.text()).strip() @property def file_name(self): return str(self.selected_file_name.text()).strip() @property def key_value(self): return self.serials_from_file def get_android_file(self): unique_dlg_name = PLUGIN_NAME + "Import Kindle for Android backup file" #takes care of automatically remembering last directory caption = "Select Kindle for Android backup file to add" filters = [("Kindle for Android backup files", ['db','ab','xml'])] files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) self.serials_from_file = [] file_name = "" if files: # find the first selected file that yields some serial numbers for filename in files: fpath = os.path.join(config_dir, filename) self.filename = os.path.basename(filename) file_serials = androidkindlekey.get_serials(fpath) if len(file_serials)>0: file_name = os.path.basename(self.filename) self.serials_from_file.extend(file_serials) self.selected_file_name.setText(file_name) def accept(self): if len(self.file_name) == 0 or len(self.key_value) == 0: errmsg = "Please choose a Kindle for Android backup file." return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) == 0 or self.key_name.isspace(): errmsg = "Please enter a key name." return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = "Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) class AddPIDDialog(QDialog): def __init__(self, parent=None,): QDialog.__init__(self, parent) self.parent = parent self.setWindowTitle("{0} {1}: Add New Mobipocket PID".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("PID:", self)) self.key_ledit = QLineEdit("", self) self.key_ledit.setToolTip("Enter a Mobipocket PID. Mobipocket PIDs are 8 or 10 characters long. Mobipocket PIDs are case-sensitive, so be sure to enter the upper and lower case letters unchanged.") 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 str(self.key_ledit.text()).strip() @property def key_value(self): return str(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): errmsg = "Please enter a Mobipocket PID or click Cancel in the dialog." return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) != 8 and len(self.key_name) != 10: errmsg = "Mobipocket PIDs must be 8 or 10 characters long. This is {0:d} characters long.".format(len(self.key_name)) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) QDialog.accept(self) ================================================ FILE: DeDRM_plugin/convert2xml.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # For use with Topaz Scripts Version 2.6 # Python 3, September 2020 # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) import sys import csv import os import getopt from struct import pack from struct import unpack class TpzDRMError(Exception): pass # Get a 7 bit encoded number from string. The most # significant byte comes first and has the high bit (8th) set def readEncodedNumber(file): flag = False c = file.read(1) if (len(c) == 0): return None data = ord(c) if data == 0xFF: flag = True c = file.read(1) if (len(c) == 0): return None data = ord(c) if data >= 0x80: datax = (data & 0x7F) while data >= 0x80 : c = file.read(1) if (len(c) == 0): return None data = c[0] datax = (datax <<7) + (data & 0x7F) data = datax if flag: data = -data return data # returns a binary string that encodes a number into 7 bits # most significant byte first which has the high bit set def encodeNumber(number): result = "" negative = False flag = 0 if number < 0 : number = -number + 1 negative = True while True: byte = number & 0x7F number = number >> 7 byte += flag result += chr(byte) flag = 0x80 if number == 0 : if (byte == 0xFF and negative == False) : result += chr(0x80) break if negative: result += chr(0xFF) return result[::-1] # create / read a length prefixed string from the file def lengthPrefixString(data): return encodeNumber(len(data))+data def readString(file): stringLength = readEncodedNumber(file) if (stringLength == None): return "" sv = file.read(stringLength) if (len(sv) != stringLength): return "" return unpack(str(stringLength)+"s",sv)[0] # convert a binary string generated by encodeNumber (7 bit encoded number) # to the value you would find inside the page*.dat files to be processed def convert(i): result = '' val = encodeNumber(i) for j in range(len(val)): c = ord(val[j:j+1]) result += '%02x' % c return result # the complete string table used to store all book text content # as well as the xml tokens and values that make sense out of it class Dictionary(object): def __init__(self, dictFile): self.filename = dictFile self.size = 0 self.fo = open(dictFile,'rb') self.stable = [] self.size = readEncodedNumber(self.fo) for i in range(self.size): self.stable.append(self.escapestr(readString(self.fo))) self.pos = 0 def escapestr(self, str): str = str.replace('&','&') str = str.replace('<','<') str = str.replace('>','>') str = str.replace('=','=') return str def lookup(self,val): if ((val >= 0) and (val < self.size)) : self.pos = val return self.stable[self.pos] else: print("Error - %d outside of string table limits" % val) raise TpzDRMError('outside of string table limits') # sys.exit(-1) def getSize(self): return self.size def getPos(self): return self.pos def dumpDict(self): for i in range(self.size): print("%d %s %s" % (i, convert(i), self.stable[i])) return # parses the xml snippets that are represented by each page*.dat file. # also parses the other0.dat file - the main stylesheet # and information used to inject the xml snippets into page*.dat files class PageParser(object): def __init__(self, filename, dict, debug, flat_xml): self.fo = open(filename,'rb') self.id = os.path.basename(filename).replace('.dat','') self.dict = dict self.debug = debug self.first_unknown = True self.flat_xml = flat_xml self.tagpath = [] self.doc = [] self.snippetList = [] # hash table used to enable the decoding process # This has all been developed by trial and error so it may still have omissions or # contain errors # Format: # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped) token_tags = { b'x' : (1, 'scalar_number', 0, 0), b'y' : (1, 'scalar_number', 0, 0), b'h' : (1, 'scalar_number', 0, 0), b'w' : (1, 'scalar_number', 0, 0), b'firstWord' : (1, 'scalar_number', 0, 0), b'lastWord' : (1, 'scalar_number', 0, 0), b'rootID' : (1, 'scalar_number', 0, 0), b'stemID' : (1, 'scalar_number', 0, 0), b'type' : (1, 'scalar_text', 0, 0), b'info' : (0, 'number', 1, 0), b'info.word' : (0, 'number', 1, 1), b'info.word.ocrText' : (1, 'text', 0, 0), b'info.word.firstGlyph' : (1, 'raw', 0, 0), b'info.word.lastGlyph' : (1, 'raw', 0, 0), b'info.word.bl' : (1, 'raw', 0, 0), b'info.word.link_id' : (1, 'number', 0, 0), b'glyph' : (0, 'number', 1, 1), b'glyph.x' : (1, 'number', 0, 0), b'glyph.y' : (1, 'number', 0, 0), b'glyph.glyphID' : (1, 'number', 0, 0), b'dehyphen' : (0, 'number', 1, 1), b'dehyphen.rootID' : (1, 'number', 0, 0), b'dehyphen.stemID' : (1, 'number', 0, 0), b'dehyphen.stemPage' : (1, 'number', 0, 0), b'dehyphen.sh' : (1, 'number', 0, 0), b'links' : (0, 'number', 1, 1), b'links.page' : (1, 'number', 0, 0), b'links.rel' : (1, 'number', 0, 0), b'links.row' : (1, 'number', 0, 0), b'links.title' : (1, 'text', 0, 0), b'links.href' : (1, 'text', 0, 0), b'links.type' : (1, 'text', 0, 0), b'links.id' : (1, 'number', 0, 0), b'paraCont' : (0, 'number', 1, 1), b'paraCont.rootID' : (1, 'number', 0, 0), b'paraCont.stemID' : (1, 'number', 0, 0), b'paraCont.stemPage' : (1, 'number', 0, 0), b'paraStems' : (0, 'number', 1, 1), b'paraStems.stemID' : (1, 'number', 0, 0), b'wordStems' : (0, 'number', 1, 1), b'wordStems.stemID' : (1, 'number', 0, 0), b'empty' : (1, 'snippets', 1, 0), b'page' : (1, 'snippets', 1, 0), b'page.class' : (1, 'scalar_text', 0, 0), b'page.pageid' : (1, 'scalar_text', 0, 0), b'page.pagelabel' : (1, 'scalar_text', 0, 0), b'page.type' : (1, 'scalar_text', 0, 0), b'page.h' : (1, 'scalar_number', 0, 0), b'page.w' : (1, 'scalar_number', 0, 0), b'page.startID' : (1, 'scalar_number', 0, 0), b'group' : (1, 'snippets', 1, 0), b'group.class' : (1, 'scalar_text', 0, 0), b'group.type' : (1, 'scalar_text', 0, 0), b'group._tag' : (1, 'scalar_text', 0, 0), b'group.orientation': (1, 'scalar_text', 0, 0), b'region' : (1, 'snippets', 1, 0), b'region.class' : (1, 'scalar_text', 0, 0), b'region.type' : (1, 'scalar_text', 0, 0), b'region.x' : (1, 'scalar_number', 0, 0), b'region.y' : (1, 'scalar_number', 0, 0), b'region.h' : (1, 'scalar_number', 0, 0), b'region.w' : (1, 'scalar_number', 0, 0), b'region.orientation' : (1, 'scalar_text', 0, 0), b'empty_text_region' : (1, 'snippets', 1, 0), b'img' : (1, 'snippets', 1, 0), b'img.x' : (1, 'scalar_number', 0, 0), b'img.y' : (1, 'scalar_number', 0, 0), b'img.h' : (1, 'scalar_number', 0, 0), b'img.w' : (1, 'scalar_number', 0, 0), b'img.src' : (1, 'scalar_number', 0, 0), b'img.color_src' : (1, 'scalar_number', 0, 0), b'img.gridSize' : (1, 'scalar_number', 0, 0), b'img.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'img.gridTopCenter' : (1, 'scalar_number', 0, 0), b'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'img.gridEndCenter' : (1, 'scalar_number', 0, 0), b'img.image_type' : (1, 'scalar_number', 0, 0), b'paragraph' : (1, 'snippets', 1, 0), b'paragraph.class' : (1, 'scalar_text', 0, 0), b'paragraph.firstWord' : (1, 'scalar_number', 0, 0), b'paragraph.lastWord' : (1, 'scalar_number', 0, 0), b'paragraph.lastWord' : (1, 'scalar_number', 0, 0), b'paragraph.gridSize' : (1, 'scalar_number', 0, 0), b'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0), b'word_semantic' : (1, 'snippets', 1, 1), b'word_semantic.type' : (1, 'scalar_text', 0, 0), b'word_semantic.class' : (1, 'scalar_text', 0, 0), b'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), b'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), b'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0), b'word' : (1, 'snippets', 1, 0), b'word.type' : (1, 'scalar_text', 0, 0), b'word.class' : (1, 'scalar_text', 0, 0), b'word.firstGlyph' : (1, 'scalar_number', 0, 0), b'word.lastGlyph' : (1, 'scalar_number', 0, 0), b'_span' : (1, 'snippets', 1, 0), b'_span.class' : (1, 'scalar_text', 0, 0), b'_span.firstWord' : (1, 'scalar_number', 0, 0), b'_span.lastWord' : (1, 'scalar_number', 0, 0), b'_span.gridSize' : (1, 'scalar_number', 0, 0), b'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'_span.gridTopCenter' : (1, 'scalar_number', 0, 0), b'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'_span.gridEndCenter' : (1, 'scalar_number', 0, 0), b'span' : (1, 'snippets', 1, 0), b'span.firstWord' : (1, 'scalar_number', 0, 0), b'span.lastWord' : (1, 'scalar_number', 0, 0), b'span.gridSize' : (1, 'scalar_number', 0, 0), b'span.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'span.gridTopCenter' : (1, 'scalar_number', 0, 0), b'span.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'span.gridEndCenter' : (1, 'scalar_number', 0, 0), b'extratokens' : (1, 'snippets', 1, 0), b'extratokens.class' : (1, 'scalar_text', 0, 0), b'extratokens.type' : (1, 'scalar_text', 0, 0), b'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), b'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), b'extratokens.gridSize' : (1, 'scalar_number', 0, 0), b'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0), b'glyph.h' : (1, 'number', 0, 0), b'glyph.w' : (1, 'number', 0, 0), b'glyph.use' : (1, 'number', 0, 0), b'glyph.vtx' : (1, 'number', 0, 1), b'glyph.len' : (1, 'number', 0, 1), b'glyph.dpi' : (1, 'number', 0, 0), b'vtx' : (0, 'number', 1, 1), b'vtx.x' : (1, 'number', 0, 0), b'vtx.y' : (1, 'number', 0, 0), b'len' : (0, 'number', 1, 1), b'len.n' : (1, 'number', 0, 0), b'book' : (1, 'snippets', 1, 0), b'version' : (1, 'snippets', 1, 0), b'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.Schema_id' : (1, 'scalar_text', 0, 0), b'version.Schema_version' : (1, 'scalar_text', 0, 0), b'version.Topaz_version' : (1, 'scalar_text', 0, 0), b'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.chapterheaders' : (1, 'scalar_text', 0, 0), b'version.creation_date' : (1, 'scalar_text', 0, 0), b'version.header_footer' : (1, 'scalar_text', 0, 0), b'version.init_from_ocr' : (1, 'scalar_text', 0, 0), b'version.letter_insertion' : (1, 'scalar_text', 0, 0), b'version.xmlinj_convert' : (1, 'scalar_text', 0, 0), b'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0), b'version.xmlinj_transform' : (1, 'scalar_text', 0, 0), b'version.findlists' : (1, 'scalar_text', 0, 0), b'version.page_num' : (1, 'scalar_text', 0, 0), b'version.page_type' : (1, 'scalar_text', 0, 0), b'version.bad_text' : (1, 'scalar_text', 0, 0), b'version.glyph_mismatch' : (1, 'scalar_text', 0, 0), b'version.margins' : (1, 'scalar_text', 0, 0), b'version.staggered_lines' : (1, 'scalar_text', 0, 0), b'version.paragraph_continuation' : (1, 'scalar_text', 0, 0), b'version.toc' : (1, 'scalar_text', 0, 0), b'stylesheet' : (1, 'snippets', 1, 0), b'style' : (1, 'snippets', 1, 0), b'style._tag' : (1, 'scalar_text', 0, 0), b'style.type' : (1, 'scalar_text', 0, 0), b'style._after_type' : (1, 'scalar_text', 0, 0), b'style._parent_type' : (1, 'scalar_text', 0, 0), b'style._after_parent_type' : (1, 'scalar_text', 0, 0), b'style.class' : (1, 'scalar_text', 0, 0), b'style._after_class' : (1, 'scalar_text', 0, 0), b'rule' : (1, 'snippets', 1, 0), b'rule.attr' : (1, 'scalar_text', 0, 0), b'rule.value' : (1, 'scalar_text', 0, 0), b'original' : (0, 'number', 1, 1), b'original.pnum' : (1, 'number', 0, 0), b'original.pid' : (1, 'text', 0, 0), b'pages' : (0, 'number', 1, 1), b'pages.ref' : (1, 'number', 0, 0), b'pages.id' : (1, 'number', 0, 0), b'startID' : (0, 'number', 1, 1), b'startID.page' : (1, 'number', 0, 0), b'startID.id' : (1, 'number', 0, 0), b'median_d' : (1, 'number', 0, 0), b'median_h' : (1, 'number', 0, 0), b'median_firsty' : (1, 'number', 0, 0), b'median_lasty' : (1, 'number', 0, 0), b'num_footers_maybe' : (1, 'number', 0, 0), b'num_footers_yes' : (1, 'number', 0, 0), b'num_headers_maybe' : (1, 'number', 0, 0), b'num_headers_yes' : (1, 'number', 0, 0), b'tracking' : (1, 'number', 0, 0), b'src' : (1, 'text', 0, 0), } # full tag path record keeping routines def tag_push(self, token): self.tagpath.append(token) def tag_pop(self): if len(self.tagpath) > 0 : self.tagpath.pop() def tagpath_len(self): return len(self.tagpath) def get_tagpath(self, i): cnt = len(self.tagpath) if i < cnt : result = self.tagpath[i] for j in range(i+1, cnt) : result += b'.' + self.tagpath[j] return result # list of absolute command byte values values that indicate # various types of loop meachanisms typically used to generate vectors cmd_list = (0x76, 0x76) # peek at and return 1 byte that is ahead by i bytes def peek(self, aheadi): c = self.fo.read(aheadi) if (len(c) == 0): return None self.fo.seek(-aheadi,1) c = c[-1:] return ord(c) # get the next value from the file being processed def getNext(self): nbyte = self.peek(1); if (nbyte == None): return None val = readEncodedNumber(self.fo) return val # format an arg by argtype def formatArg(self, arg, argtype): if (argtype == 'text') or (argtype == 'scalar_text') : result = self.dict.lookup(arg) elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') : result = arg elif (argtype == 'snippets') : result = arg else : print("Error Unknown argtype %s" % argtype) sys.exit(-2) return result # process the next tag token, recursively handling subtags, # arguments, and commands def procToken(self, token): known_token = False self.tag_push(token) if self.debug : print('Processing: ', self.get_tagpath(0)) cnt = self.tagpath_len() for j in range(cnt): tkn = self.get_tagpath(j) if tkn in self.token_tags : num_args = self.token_tags[tkn][0] argtype = self.token_tags[tkn][1] subtags = self.token_tags[tkn][2] splcase = self.token_tags[tkn][3] ntags = -1 known_token = True break if known_token : # handle subtags if present subtagres = [] if (splcase == 1): # this type of tag uses of escape marker 0x74 indicate subtag count if self.peek(1) == 0x74: skip = readEncodedNumber(self.fo) subtags = 1 num_args = 0 if (subtags == 1): ntags = readEncodedNumber(self.fo) if self.debug : print('subtags: ', token , ' has ' , str(ntags)) for j in range(ntags): val = readEncodedNumber(self.fo) subtagres.append(self.procToken(self.dict.lookup(val))) # arguments can be scalars or vectors of text or numbers argres = [] if num_args > 0 : firstarg = self.peek(1) if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'): # single argument is a variable length vector of data arg = readEncodedNumber(self.fo) argres = self.decodeCMD(arg,argtype) else : # num_arg scalar arguments for i in range(num_args): argres.append(self.formatArg(readEncodedNumber(self.fo), argtype)) # build the return tag result = [] tkn = self.get_tagpath(0) result.append(tkn) result.append(subtagres) result.append(argtype) result.append(argres) self.tag_pop() return result # all tokens that need to be processed should be in the hash # table if it may indicate a problem, either new token # or an out of sync condition else: result = [] if (self.debug or self.first_unknown): print('Unknown Token:', token) self.first_unknown = False self.tag_pop() return result # special loop used to process code snippets # it is NEVER used to format arguments. # builds the snippetList def doLoop72(self, argtype): cnt = readEncodedNumber(self.fo) if self.debug : result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n' result += 'of the document is indicated by snippet number sets at the\n' result += 'end of each snippet. \n' print(result) for i in range(cnt): if self.debug: print('Snippet:',str(i)) snippet = [] snippet.append(i) val = readEncodedNumber(self.fo) snippet.append(self.procToken(self.dict.lookup(val))) self.snippetList.append(snippet) return # general loop code gracisouly submitted by "skindle" - thank you! def doLoop76Mode(self, argtype, cnt, mode): result = [] adj = 0 if mode & 1: adj = readEncodedNumber(self.fo) mode = mode >> 1 x = [] for i in range(cnt): x.append(readEncodedNumber(self.fo) - adj) for i in range(mode): for j in range(1, cnt): x[j] = x[j] + x[j - 1] for i in range(cnt): result.append(self.formatArg(x[i],argtype)) return result # dispatches loop commands bytes with various modes # The 0x76 style loops are used to build vectors # This was all derived by trial and error and # new loop types may exist that are not handled here # since they did not appear in the test cases def decodeCMD(self, cmd, argtype): if (cmd == 0x76): # loop with cnt, and mode to control loop styles cnt = readEncodedNumber(self.fo) mode = readEncodedNumber(self.fo) if self.debug : print('Loop for', cnt, 'with mode', mode, ': ') return self.doLoop76Mode(argtype, cnt, mode) if self.dbug: print("Unknown command", cmd) result = [] return result # add full tag path to injected snippets def updateName(self, tag, prefix): name = tag[0] subtagList = tag[1] argtype = tag[2] argList = tag[3] nname = prefix + b'.' + name nsubtaglist = [] for j in subtagList: nsubtaglist.append(self.updateName(j,prefix)) ntag = [] ntag.append(nname) ntag.append(nsubtaglist) ntag.append(argtype) ntag.append(argList) return ntag # perform depth first injection of specified snippets into this one def injectSnippets(self, snippet): snipno, tag = snippet name = tag[0] subtagList = tag[1] argtype = tag[2] argList = tag[3] nsubtagList = [] if len(argList) > 0 : for j in argList: asnip = self.snippetList[j] aso, atag = self.injectSnippets(asnip) atag = self.updateName(atag, name) nsubtagList.append(atag) argtype='number' argList=[] if len(nsubtagList) > 0 : subtagList.extend(nsubtagList) tag = [] tag.append(name) tag.append(subtagList) tag.append(argtype) tag.append(argList) snippet = [] snippet.append(snipno) snippet.append(tag) return snippet # format the tag for output def formatTag(self, node): name = node[0] subtagList = node[1] argtype = node[2] argList = node[3] fullpathname = name.split(b'.') nodename = fullpathname.pop() ilvl = len(fullpathname) indent = b' ' * (3 * ilvl) rlst = [] rlst.append(indent + b'<' + nodename + b'>') if len(argList) > 0: alst = [] for j in argList: if (argtype == b'text') or (argtype == b'scalar_text') : alst.append(j + b'|') else : alst.append(str(j).encode('utf-8') + b',') argres = b"".join(alst) argres = argres[0:-1] if argtype == b'snippets' : rlst.append(b'snippets:' + argres) else : rlst.append(argres) if len(subtagList) > 0 : rlst.append(b'\n') for j in subtagList: if len(j) > 0 : rlst.append(self.formatTag(j)) rlst.append(indent + b'\n') else: rlst.append(b'\n') return b"".join(rlst) # flatten tag def flattenTag(self, node): name = node[0] subtagList = node[1] argtype = node[2] argList = node[3] rlst = [] rlst.append(name) if (len(argList) > 0): alst = [] for j in argList: if (argtype == 'text') or (argtype == 'scalar_text') : alst.append(j + b'|') else : alst.append(str(j).encode('utf-8') + b'|') argres = b"".join(alst) argres = argres[0:-1] if argtype == b'snippets' : rlst.append(b'.snippets=' + argres) else : rlst.append(b'=' + argres) rlst.append(b'\n') for j in subtagList: if len(j) > 0 : rlst.append(self.flattenTag(j)) return b"".join(rlst) # reduce create xml output def formatDoc(self, flat_xml): rlst = [] for j in self.doc : if len(j) > 0: if flat_xml: rlst.append(self.flattenTag(j)) else: rlst.append(self.formatTag(j)) result = b"".join(rlst) if self.debug : print(result) return result # main loop - parse the page.dat files # to create structured document and snippets # FIXME: value at end of magic appears to be a subtags count # but for what? For now, inject an 'info" tag as it is in # every dictionary and seems close to what is meant # The alternative is to special case the last _ "0x5f" to mean something def process(self): # peek at the first bytes to see what type of file it is magic = self.fo.read(9) if (magic[0:1] == b'p') and (magic[2:9] == b'marker_'): first_token = b'info' elif (magic[0:1] == b'p') and (magic[2:9] == b'__PAGE_'): skip = self.fo.read(2) first_token = b'info' elif (magic[0:1] == b'p') and (magic[2:8] == b'_PAGE_'): first_token = b'info' elif (magic[0:1] == b'g') and (magic[2:9] == b'__GLYPH'): skip = self.fo.read(3) first_token = b'info' else : # other0.dat file first_token = None self.fo.seek(-9,1) # main loop to read and build the document tree while True: if first_token != None : # use "inserted" first token 'info' for page and glyph files tag = self.procToken(first_token) if len(tag) > 0 : self.doc.append(tag) first_token = None v = self.getNext() if (v == None): break if (v == 0x72): self.doLoop72(b'number') elif (v > 0) and (v < self.dict.getSize()) : tag = self.procToken(self.dict.lookup(v)) if len(tag) > 0 : self.doc.append(tag) else: if self.debug: print("Main Loop: Unknown value: %x" % v) if (v == 0): if (self.peek(1) == 0x5f): skip = self.fo.read(1) first_token = b'info' # now do snippet injection if len(self.snippetList) > 0 : if self.debug : print('Injecting Snippets:') snippet = self.injectSnippets(self.snippetList[0]) snipno = snippet[0] tag_add = snippet[1] if self.debug : print(self.formatTag(tag_add)) if len(tag_add) > 0: self.doc.append(tag_add) # handle generation of xml output xmlpage = self.formatDoc(self.flat_xml) return xmlpage def fromData(dict, fname): flat_xml = True debug = True pp = PageParser(fname, dict, debug, flat_xml) xmlpage = pp.process() return xmlpage def getXML(dict, fname): flat_xml = False debug = True pp = PageParser(fname, dict, debug, flat_xml) xmlpage = pp.process() return xmlpage def usage(): print('Usage: ') print(' convert2xml.py dict0000.dat infile.dat ') print(' ') print(' Options:') print(' -h print this usage help message ') print(' -d turn on debug output to check for potential errors ') print(' --flat-xml output the flattened xml page description only ') print(' ') print(' This program will attempt to convert a page*.dat file or ') print(' glyphs*.dat file, using the dict0000.dat file, to its xml description. ') print(' ') print(' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ') print(' the *.dat files from a Topaz format e-book.') # # Main # def main(argv): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) dictFile = "" pageFile = "" debug = True flat_xml = False printOutput = False if len(argv) == 0: printOutput = True argv = sys.argv try: opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"]) except getopt.GetoptError as err: # print help information and exit: print(str(err)) # will print something like "option -a not recognized" usage() sys.exit(2) if len(opts) == 0 and len(args) == 0 : usage() sys.exit(2) for o, a in opts: if o =="-d": debug=True if o =="-h": usage() sys.exit(0) if o =="--flat-xml": flat_xml = True dictFile, pageFile = args[0], args[1] # read in the string table dictionary dict = Dictionary(dictFile) # dict.dumpDict() # create a page parser pp = PageParser(pageFile, dict, debug, flat_xml) xmlpage = pp.process() if printOutput: print(xmlpage) return 0 return xmlpage if __name__ == '__main__': sys.exit(main('')) ================================================ FILE: DeDRM_plugin/epubtest.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # # Changelog drmcheck # 1.00 - Initial version, with code from various other scripts # 1.01 - Moved authorship announcement to usage section. # # Changelog epubtest # 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf # 1.01 - Added routine for use by Windows DeDRM # 2.00 - Python 3, September 2020 # # Written in 2011 by Paul Durrant # Released with unlicense. See http://unlicense.org/ # ############################################################################# # # This is free and unencumbered software released into the public domain. # # Anyone is free to copy, modify, publish, use, compile, sell, or # distribute this software, either in source code form or as a compiled # binary, for any purpose, commercial or non-commercial, and by any # means. # # In jurisdictions that recognize copyright laws, the author or authors # of this software dedicate any and all copyright interest in the # software to the public domain. We make this dedication for the benefit # of the public at large and to the detriment of our heirs and # successors. We intend this dedication to be an overt act of # relinquishment in perpetuity of all present and future rights to this # software under copyright law. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # ############################################################################# # # It's still polite to give attribution if you do reuse this code. # __version__ = '2.0' import sys, struct, os, traceback import zlib import zipfile import xml.etree.ElementTree as etree NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["epubtest.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] _FILENAME_LEN_OFFSET = 26 _EXTRA_LEN_OFFSET = 28 _FILENAME_OFFSET = 30 _MAX_SIZE = 64 * 1024 def uncompress(cmpdata): dc = zlib.decompressobj(-15) data = '' while len(cmpdata) > 0: if len(cmpdata) > _MAX_SIZE : newdata = cmpdata[0:_MAX_SIZE] cmpdata = cmpdata[_MAX_SIZE:] else: newdata = cmpdata cmpdata = '' newdata = dc.decompress(newdata) unprocessed = dc.unconsumed_tail if len(unprocessed) == 0: newdata += dc.flush() data += newdata cmpdata += unprocessed unprocessed = '' return data def getfiledata(file, zi): # get file name length and exta data length to find start of file data local_header_offset = zi.header_offset file.seek(local_header_offset + _FILENAME_LEN_OFFSET) leninfo = file.read(2) local_name_length, = struct.unpack(' 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["mobidedrm.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] Des = None if iswindows: # first try with pycrypto if inCalibre: from calibre_plugins.dedrm import pycrypto_des else: import pycrypto_des Des = pycrypto_des.load_pycrypto() if Des == None: # they try with openssl if inCalibre: from calibre_plugins.dedrm import openssl_des else: import openssl_des Des = openssl_des.load_libcrypto() else: # first try with openssl if inCalibre: from calibre_plugins.dedrm import openssl_des else: import openssl_des Des = openssl_des.load_libcrypto() if Des == None: # then try with pycrypto if inCalibre: from calibre_plugins.dedrm import pycrypto_des else: import pycrypto_des Des = pycrypto_des.load_pycrypto() # if that did not work then use pure python implementation # of DES and try to speed it up with Psycho if Des == None: if inCalibre: from calibre_plugins.dedrm import python_des else: import python_des Des = python_des.Des # Import Psyco if available try: # http://psyco.sourceforge.net import psyco psyco.full() except ImportError: pass try: from hashlib import sha1 except ImportError: # older Python release import sha sha1 = lambda s: sha.new(s) import cgi import logging logging.basicConfig() #logging.basicConfig(level=logging.DEBUG) class Sectionizer(object): bkType = "Book" def __init__(self, filename, ident): self.contents = open(filename, 'rb').read() self.header = self.contents[0:72] self.num_sections, = struct.unpack('>H', self.contents[76:78]) # Dictionary or normal content (TODO: Not hard-coded) if self.header[0x3C:0x3C+8] != ident: if self.header[0x3C:0x3C+8] == b"PDctPPrs": self.bkType = "Dict" else: raise ValueError('Invalid file format') self.sections = [] for i in range(self.num_sections): offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8]) flags, val = a1, a2<<16|a3<<8|a4 self.sections.append( (offset, flags, val) ) def loadSection(self, section): if section + 1 == self.num_sections: end_off = len(self.contents) else: end_off = self.sections[section + 1][0] off = self.sections[section][0] return self.contents[off:end_off] # cleanup unicode filenames # borrowed from calibre from calibre/src/calibre/__init__.py # added in removal of control (<32) chars # and removal of . at start and end # and with some (heavily edited) code from Paul Durrant's kindlenamer.py def sanitizeFileName(name): # substitute filename unfriendly characters name = name.replace("<","[").replace(">","]").replace(" : "," – ").replace(": "," – ").replace(":","—").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'") # delete control characters name = "".join(char for char in name if ord(char)>=32) # white space to single space, delete leading and trailing while space name = re.sub(r"\s", " ", name).strip() # remove leading dots while len(name)>0 and name[0] == ".": name = name[1:] # remove trailing dots (Windows doesn't like them) if name.endswith("."): name = name[:-1] return name def fixKey(key): def fixByte(b): return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) return bytes([fixByte(a) for a in key]) def deXOR(text, sp, table): r='' j = sp for i in range(len(text)): r += chr(ord(table[j]) ^ ord(text[i])) j = j + 1 if j == len(table): j = 0 return r class EreaderProcessor(object): def __init__(self, sect, user_key): self.section_reader = sect.loadSection data = self.section_reader(0) version, = struct.unpack('>H', data[0:2]) self.version = version logging.info('eReader file format version %s', version) if version != 272 and version != 260 and version != 259: raise ValueError('incorrect eReader version %d (error 1)' % version) data = self.section_reader(1) self.data = data des = Des(fixKey(data[0:8])) cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:])) if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200: raise ValueError('incorrect eReader version (error 2)') input = des.decrypt(data[-cookie_size:]) def unshuff(data, shuf): r = [0] * len(data) j = 0 for i in range(len(data)): j = (j + shuf) % len(data) r[j] = data[i] assert len(bytes(r)) == len(data) return bytes(r) r = unshuff(input[0:-8], cookie_shuf) drm_sub_version = struct.unpack('>H', r[0:2])[0] self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1 self.num_image_pages = struct.unpack('>H', r[26:26+2])[0] self.first_image_page = struct.unpack('>H', r[24:24+2])[0] # Default values self.num_footnote_pages = 0 self.num_sidebar_pages = 0 self.first_footnote_page = -1 self.first_sidebar_page = -1 if self.version == 272: self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0] self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0] if (sect.bkType == "Book"): self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0] self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0] # self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0] # self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0] # self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0] # self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0] # self.num_link_pages = struct.unpack('>H', r[30:30+2])[0] # self.first_link_page = struct.unpack('>H', r[28:28+2])[0] # self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0] # self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0] # **before** data record 1 was decrypted and unshuffled, it contained data # to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc self.xortable_offset = struct.unpack('>H', r[40:40+2])[0] self.xortable_size = struct.unpack('>H', r[42:42+2])[0] self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size] else: # Nothing needs to be done pass # self.num_bookinfo_pages = 0 # self.num_chapter_pages = 0 # self.num_link_pages = 0 # self.num_xtextsize_pages = 0 # self.first_bookinfo_page = -1 # self.first_chapter_page = -1 # self.first_link_page = -1 # self.first_xtextsize_page = -1 logging.debug('self.num_text_pages %d', self.num_text_pages) logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page) logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page) self.flags = struct.unpack('>L', r[4:8])[0] reqd_flags = (1<<9) | (1<<7) | (1<<10) if (self.flags & reqd_flags) != reqd_flags: print("Flags: 0x%X" % self.flags) raise ValueError('incompatible eReader file') des = Des(fixKey(user_key)) if version == 259: if drm_sub_version != 7: raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) encrypted_key_sha = r[44:44+20] encrypted_key = r[64:64+8] elif version == 260: if drm_sub_version != 13 and drm_sub_version != 11: raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version) if drm_sub_version == 13: encrypted_key = r[44:44+8] encrypted_key_sha = r[52:52+20] else: encrypted_key = r[64:64+8] encrypted_key_sha = r[44:44+20] elif version == 272: encrypted_key = r[172:172+8] encrypted_key_sha = r[56:56+20] self.content_key = des.decrypt(encrypted_key) if sha1(self.content_key).digest() != encrypted_key_sha: raise ValueError('Incorrect Name and/or Credit Card') def getNumImages(self): return self.num_image_pages def getImage(self, i): sect = self.section_reader(self.first_image_page + i) name = sect[4:4+32].strip(b'\0') data = sect[62:] return sanitizeFileName(name.decode('windows-1252')), data # def getChapterNamePMLOffsetData(self): # cv = '' # if self.num_chapter_pages > 0: # for i in xrange(self.num_chapter_pages): # chaps = self.section_reader(self.first_chapter_page + i) # j = i % self.xortable_size # offname = deXOR(chaps, j, self.xortable) # offset = struct.unpack('>L', offname[0:4])[0] # name = offname[4:].strip('\0') # cv += '%d|%s\n' % (offset, name) # return cv # def getLinkNamePMLOffsetData(self): # lv = '' # if self.num_link_pages > 0: # for i in xrange(self.num_link_pages): # links = self.section_reader(self.first_link_page + i) # j = i % self.xortable_size # offname = deXOR(links, j, self.xortable) # offset = struct.unpack('>L', offname[0:4])[0] # name = offname[4:].strip('\0') # lv += '%d|%s\n' % (offset, name) # return lv # def getExpandedTextSizesData(self): # ts = '' # if self.num_xtextsize_pages > 0: # tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable) # for i in xrange(self.num_text_pages): # xsize = struct.unpack('>H', tsize[0:2])[0] # ts += "%d\n" % xsize # tsize = tsize[2:] # return ts # def getBookInfo(self): # bkinfo = '' # if self.num_bookinfo_pages > 0: # info = self.section_reader(self.first_bookinfo_page) # bkinfo = deXOR(info, 0, self.xortable) # bkinfo = bkinfo.replace('\0','|') # bkinfo += '\n' # return bkinfo def getText(self): des = Des(fixKey(self.content_key)) r = b'' for i in range(self.num_text_pages): logging.debug('get page %d', i) r += zlib.decompress(des.decrypt(self.section_reader(1 + i))) # now handle footnotes pages if self.num_footnote_pages > 0: r += '\n' # the record 0 of the footnote section must pass through the Xor Table to make it useful sect = self.section_reader(self.first_footnote_page) fnote_ids = deXOR(sect, 0, self.xortable) # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated des = Des(fixKey(self.content_key)) for i in range(1,self.num_footnote_pages): logging.debug('get footnotepage %d', i) id_len = ord(fnote_ids[2]) id = fnote_ids[3:3+id_len] fmarker = '\n' % id fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) fmarker += '\n\n' r += fmarker fnote_ids = fnote_ids[id_len+4:] # TODO: Handle dictionary index (?) pages - which are also marked as # sidebar_pages (?). For now dictionary sidebars are ignored # For dictionaries - record 0 is null terminated strings, followed by # blocks of around 62000 bytes and a final block. Not sure of the # encoding # now handle sidebar pages if self.num_sidebar_pages > 0: r += '\n' # the record 0 of the sidebar section must pass through the Xor Table to make it useful sect = self.section_reader(self.first_sidebar_page) sbar_ids = deXOR(sect, 0, self.xortable) # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated des = Des(fixKey(self.content_key)) for i in range(1,self.num_sidebar_pages): id_len = ord(sbar_ids[2]) id = sbar_ids[3:3+id_len] smarker = '\n' % id smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i))) smarker += '\n\n' r += smarker sbar_ids = sbar_ids[id_len+4:] return r def cleanPML(pml): # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) pml2 = pml for k in range(128,256): pml2 = pml2.replace(bytes([k]), b'\\a%03d' % k) return pml2 def decryptBook(infile, outpath, make_pmlz, user_key): bookname = os.path.splitext(os.path.basename(infile))[0] if make_pmlz: # outpath is actually pmlz name pmlzname = outpath outdir = tempfile.mkdtemp() imagedirpath = os.path.join(outdir,"images") else: pmlzname = None outdir = outpath imagedirpath = os.path.join(outdir,bookname + "_img") try: if not os.path.exists(outdir): os.makedirs(outdir) print("Decoding File") sect =Sectionizer(infile, b'PNRdPPrs') er = EreaderProcessor(sect, user_key) if er.getNumImages() > 0: print("Extracting images") if not os.path.exists(imagedirpath): os.makedirs(imagedirpath) for i in range(er.getNumImages()): name, contents = er.getImage(i) open(os.path.join(imagedirpath, name), 'wb').write(contents) print("Extracting pml") pml_string = er.getText() pmlfilename = bookname + ".pml" open(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string)) if pmlzname is not None: import zipfile import shutil print("Creating PMLZ file {0}".format(os.path.basename(pmlzname))) myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False) list = os.listdir(outdir) for filename in list: localname = filename filePath = os.path.join(outdir,filename) if os.path.isfile(filePath): myZipFile.write(filePath, localname) elif os.path.isdir(filePath): imageList = os.listdir(filePath) localimgdir = os.path.basename(filePath) for image in imageList: localname = os.path.join(localimgdir,image) imagePath = os.path.join(filePath,image) if os.path.isfile(imagePath): myZipFile.write(imagePath, localname) myZipFile.close() # remove temporary directory shutil.rmtree(outdir, True) print("Output is {0}".format(pmlzname)) else: print("Output is in {0}".format(outdir)) print("done") except ValueError as e: print("Error: {0}".format(e)) traceback.print_exc() return 1 return 0 def usage(): print("Converts DRMed eReader books to PML Source") print("Usage:") print(" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number") print(" ") print("Options: ") print(" -h prints this message") print(" -p create PMLZ instead of source folder") print(" --make-pmlz create PMLZ instead of source folder") print(" ") print("Note:") print(" if outpath is ommitted, creates source in 'infile_Source' folder") print(" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'") print(" if source folder created, images are in infile_img folder") print(" if pmlz file created, images are in images folder") print(" It's enough to enter the last 8 digits of the credit card number") return def getuser_key(name,cc): newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9') cc = cc.replace(" ","") return struct.pack('>LL', binascii.crc32(bytes(newname.encode('utf-8'))) & 0xffffffff, binascii.crc32(bytes(cc[-8:].encode('utf-8'))) & 0xffffffff) def cli_main(): print("eRdr2Pml v{0}. Copyright © 2009–2020 The Dark Reverser et al.".format(__version__)) argv=unicode_argv() try: opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) except getopt.GetoptError as err: print(err.args[0]) usage() return 1 make_pmlz = False for o, a in opts: if o == "-h": usage() return 0 elif o == "-p": make_pmlz = True elif o == "--make-pmlz": make_pmlz = True if len(args)!=3 and len(args)!=4: usage() return 1 if len(args)==3: infile, name, cc = args if make_pmlz: outpath = os.path.splitext(infile)[0] + ".pmlz" else: outpath = os.path.splitext(infile)[0] + "_Source" elif len(args)==4: infile, outpath, name, cc = args print(binascii.b2a_hex(getuser_key(name,cc))) return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc)) if __name__ == "__main__": sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) sys.exit(cli_main()) ================================================ FILE: DeDRM_plugin/flatxml2html.py ================================================ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # For use with Topaz Scripts Version 2.6 import sys import csv import os import math import getopt import functools from struct import pack from struct import unpack class DocParser(object): def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage): self.id = os.path.basename(fileid).replace('.dat','') self.svgcount = 0 self.docList = flatxml.split(b'\n') self.docSize = len(self.docList) self.classList = {} self.bookDir = bookDir self.gdict = gdict tmpList = classlst.split('\n') for pclass in tmpList: if pclass != b'': # remove the leading period from the css name cname = pclass[1:] self.classList[cname] = True self.fixedimage = fixedimage self.ocrtext = [] self.link_id = [] self.link_title = [] self.link_page = [] self.link_href = [] self.link_type = [] self.dehyphen_rootid = [] self.paracont_stemid = [] self.parastems_stemid = [] def getGlyph(self, gid): result = '' id='id="gl%d"' % gid return self.gdict.lookup(id) def glyphs_to_image(self, glyphList): def extract(path, key): b = path.find(key) + len(key) e = path.find(' ',b) return int(path[b:e]) svgDir = os.path.join(self.bookDir,'svg') imgDir = os.path.join(self.bookDir,'img') imgname = self.id + '_%04d.svg' % self.svgcount imgfile = os.path.join(imgDir,imgname) # get glyph information gxList = self.getData(b'info.glyph.x',0,-1) gyList = self.getData(b'info.glyph.y',0,-1) gidList = self.getData(b'info.glyph.glyphID',0,-1) gids = [] maxws = [] maxhs = [] xs = [] ys = [] gdefs = [] # get path defintions, positions, dimensions for each glyph # that makes up the image, and find min x and min y to reposition origin minx = -1 miny = -1 for j in glyphList: gid = gidList[j] gids.append(gid) xs.append(gxList[j]) if minx == -1: minx = gxList[j] else : minx = min(minx, gxList[j]) ys.append(gyList[j]) if miny == -1: miny = gyList[j] else : miny = min(miny, gyList[j]) path = self.getGlyph(gid) gdefs.append(path) maxws.append(extract(path,'width=')) maxhs.append(extract(path,'height=')) # change the origin to minx, miny and calc max height and width maxw = maxws[0] + xs[0] - minx maxh = maxhs[0] + ys[0] - miny for j in range(0, len(xs)): xs[j] = xs[j] - minx ys[j] = ys[j] - miny maxw = max( maxw, (maxws[j] + xs[j]) ) maxh = max( maxh, (maxhs[j] + ys[j]) ) # open the image file for output ifile = open(imgfile,'w') ifile.write('\n') ifile.write('\n') ifile.write('\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh)) ifile.write('\n') for j in range(0,len(gdefs)): ifile.write(gdefs[j]) ifile.write('\n') for j in range(0,len(gids)): ifile.write('\n' % (gids[j], xs[j], ys[j])) ifile.write('') ifile.close() return 0 # return tag at line pos in document def lineinDoc(self, pos) : if (pos >= 0) and (pos < self.docSize) : item = self.docList[pos] if item.find(b'=') >= 0: (name, argres) = item.split(b'=',1) else : name = item argres = b'' return name, argres # find tag in doc if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None if end == -1 : end = self.docSize else: end = min(self.docSize, end) foundat = -1 for j in range(pos, end): item = self.docList[j] if item.find(b'=') >= 0: (name, argres) = item.split(b'=',1) else : name = item argres = '' if (isinstance(tagpath,str)): tagpath = tagpath.encode('utf-8') if name.endswith(tagpath) : result = argres foundat = j break return foundat, result # return list of start positions for the tagpath def posinDoc(self, tagpath): startpos = [] pos = 0 res = "" while res != None : (foundpos, res) = self.findinDoc(tagpath, pos, -1) if res != None : startpos.append(foundpos) pos = foundpos + 1 return startpos # returns a vector of integers for the tagpath def getData(self, tagpath, pos, end): argres=[] (foundat, argt) = self.findinDoc(tagpath, pos, end) if (argt != None) and (len(argt) > 0) : argList = argt.split(b'|') argres = [ int(strval) for strval in argList] return argres # get the class def getClass(self, pclass): nclass = pclass # class names are an issue given topaz may start them with numerals (not allowed), # use a mix of cases (which cause some browsers problems), and actually # attach numbers after "_reclustered*" to the end to deal classeses that inherit # from a base class (but then not actually provide all of these _reclustereed # classes in the stylesheet! # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass # that exists in the stylesheet first, and then adding this specific class # after # also some class names have spaces in them so need to convert to dashes if nclass != None : nclass = nclass.replace(b' ',b'-') classres = b'' nclass = nclass.lower() nclass = b'cl-' + nclass baseclass = b'' # graphic is the base class for captions if nclass.find(b'cl-cap-') >=0 : classres = b'graphic' + b' ' else : # strip to find baseclass p = nclass.find(b'_') if p > 0 : baseclass = nclass[0:p] if baseclass in self.classList: classres += baseclass + b' ' classres += nclass nclass = classres return nclass # develop a sorted description of the starting positions of # groups and regions on the page, as well as the page type def PageDescription(self): def compare(x, y): (xtype, xval) = x (ytype, yval) = y if xval > yval: return 1 if xval == yval: return 0 return -1 result = [] (pos, pagetype) = self.findinDoc(b'page.type',0,-1) groupList = self.posinDoc(b'page.group') groupregionList = self.posinDoc(b'page.group.region') pageregionList = self.posinDoc(b'page.region') # integrate into one list for j in groupList: result.append(('grpbeg',j)) for j in groupregionList: result.append(('gregion',j)) for j in pageregionList: result.append(('pregion',j)) result.sort(key=functools.cmp_to_key(compare)) # insert group end and page end indicators inGroup = False j = 0 while True: if j == len(result): break rtype = result[j][0] rval = result[j][1] if not inGroup and (rtype == 'grpbeg') : inGroup = True j = j + 1 elif inGroup and (rtype in ('grpbeg', 'pregion')): result.insert(j,('grpend',rval)) inGroup = False else: j = j + 1 if inGroup: result.append(('grpend',-1)) result.append(('pageend', -1)) return pagetype, result # build a description of the paragraph def getParaDescription(self, start, end, regtype): result = [] # paragraph (pos, pclass) = self.findinDoc(b'paragraph.class',start,end) pclass = self.getClass(pclass) # if paragraph uses extratokens (extra glyphs) then make it fixed (pos, extraglyphs) = self.findinDoc(b'paragraph.extratokens',start,end) # build up a description of the paragraph in result and return it # first check for the basic - all words paragraph (pos, sfirst) = self.findinDoc(b'paragraph.firstWord',start,end) (pos, slast) = self.findinDoc(b'paragraph.lastWord',start,end) if (sfirst != None) and (slast != None) : first = int(sfirst) last = int(slast) makeImage = (regtype == b'vertical') or (regtype == b'table') makeImage = makeImage or (extraglyphs != None) if self.fixedimage: makeImage = makeImage or (regtype == b'fixed') if (pclass != None): makeImage = makeImage or (pclass.find(b'.inverted') >= 0) if self.fixedimage : makeImage = makeImage or (pclass.find(b'cl-f-') >= 0) # before creating an image make sure glyph info exists gidList = self.getData(b'info.glyph.glyphID',0,-1) makeImage = makeImage & (len(gidList) > 0) if not makeImage : # standard all word paragraph for wordnum in range(first, last): result.append(('ocr', wordnum)) return pclass, result # convert paragraph to svg image # translate first and last word into first and last glyphs # and generate inline image and include it glyphList = [] firstglyphList = self.getData(b'word.firstGlyph',0,-1) gidList = self.getData(b'info.glyph.glyphID',0,-1) firstGlyph = firstglyphList[first] if last < len(firstglyphList): lastGlyph = firstglyphList[last] else : lastGlyph = len(gidList) # handle case of white sapce paragraphs with no actual glyphs in them # by reverting to text based paragraph if firstGlyph >= lastGlyph: # revert to standard text based paragraph for wordnum in range(first, last): result.append(('ocr', wordnum)) return pclass, result for glyphnum in range(firstGlyph, lastGlyph): glyphList.append(glyphnum) # include any extratokens if they exist (pos, sfg) = self.findinDoc(b'extratokens.firstGlyph',start,end) (pos, slg) = self.findinDoc(b'extratokens.lastGlyph',start,end) if (sfg != None) and (slg != None): for glyphnum in range(int(sfg), int(slg)): glyphList.append(glyphnum) num = self.svgcount self.glyphs_to_image(glyphList) self.svgcount += 1 result.append(('svg', num)) return pclass, result # this type of paragraph may be made up of multiple spans, inline # word monograms (images), and words with semantic meaning, # plus glyphs used to form starting letter of first word # need to parse this type line by line line = start + 1 word_class = '' # if end is -1 then we must search to end of document if end == -1 : end = self.docSize # seems some xml has last* coming before first* so we have to # handle any order sp_first = -1 sp_last = -1 gl_first = -1 gl_last = -1 ws_first = -1 ws_last = -1 word_class = '' word_semantic_type = '' while (line < end) : (name, argres) = self.lineinDoc(line) if name.endswith(b'span.firstWord') : sp_first = int(argres) elif name.endswith(b'span.lastWord') : sp_last = int(argres) elif name.endswith(b'word.firstGlyph') : gl_first = int(argres) elif name.endswith(b'word.lastGlyph') : gl_last = int(argres) elif name.endswith(b'word_semantic.firstWord'): ws_first = int(argres) elif name.endswith(b'word_semantic.lastWord'): ws_last = int(argres) elif name.endswith(b'word.class'): # we only handle spaceafter word class try: (cname, space) = argres.split(b'-',1) if space == b'' : space = b'0' if (cname == b'spaceafter') and (int(space) > 0) : word_class = 'sa' except: pass elif name.endswith(b'word.img.src'): result.append(('img' + word_class, int(argres))) word_class = '' elif name.endswith(b'region.img.src'): result.append(('img' + word_class, int(argres))) if (sp_first != -1) and (sp_last != -1): for wordnum in range(sp_first, sp_last): result.append(('ocr', wordnum)) sp_first = -1 sp_last = -1 if (gl_first != -1) and (gl_last != -1): glyphList = [] for glyphnum in range(gl_first, gl_last): glyphList.append(glyphnum) num = self.svgcount self.glyphs_to_image(glyphList) self.svgcount += 1 result.append(('svg', num)) gl_first = -1 gl_last = -1 if (ws_first != -1) and (ws_last != -1): for wordnum in range(ws_first, ws_last): result.append(('ocr', wordnum)) ws_first = -1 ws_last = -1 line += 1 return pclass, result def buildParagraph(self, pclass, pdesc, type, regtype) : parares = '' sep ='' classres = '' if pclass : classres = ' class="' + pclass.decode('utf-8') + '"' br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') handle_links = len(self.link_id) > 0 if (type == 'full') or (type == 'begin') : parares += '' if (type == 'end'): parares += ' ' lstart = len(parares) cnt = len(pdesc) for j in range( 0, cnt) : (wtype, num) = pdesc[j] if wtype == 'ocr' : try: word = self.ocrtext[num] except: word = "" sep = ' ' if handle_links: link = self.link_id[num] if (link > 0): linktype = self.link_type[link-1] title = self.link_title[link-1] if (title == b"") or (parares.rfind(title.decode('utf-8')) < 0): title=parares[lstart:].encode('utf-8') if linktype == 'external' : linkhref = self.link_href[link-1] linkhtml = '' % linkhref else : if len(self.link_page) >= link : ptarget = self.link_page[link-1] - 1 linkhtml = '' % ptarget else : # just link to the current page linkhtml = '' linkhtml += title.decode('utf-8') linkhtml += '' pos = parares.rfind(title.decode('utf-8')) if pos >= 0: parares = parares[0:pos] + linkhtml + parares[pos+len(title):] else : parares += linkhtml lstart = len(parares) if word == b'_link_' : word = b'' elif (link < 0) : if word == b'_link_' : word = b'' if word == b'_lb_': if ((num-1) in self.dehyphen_rootid ) or handle_links: word = b'' sep = '' elif br_lb : word = b'
\n' sep = '' else : word = b'\n' sep = '' if num in self.dehyphen_rootid : word = word[0:-1] sep = '' parares += word.decode('utf-8') + sep elif wtype == 'img' : sep = '' parares += '' % num parares += sep elif wtype == 'imgsa' : sep = ' ' parares += '' % num parares += sep elif wtype == 'svg' : sep = '' parares += '' % num parares += sep if len(sep) > 0 : parares = parares[0:-1] if (type == 'full') or (type == 'end') : parares += '

' return parares def buildTOCEntry(self, pdesc) : parares = '' sep ='' tocentry = '' handle_links = len(self.link_id) > 0 lstart = 0 cnt = len(pdesc) for j in range( 0, cnt) : (wtype, num) = pdesc[j] if wtype == 'ocr' : word = self.ocrtext[num].decode('utf-8') sep = ' ' if handle_links: link = self.link_id[num] if (link > 0): linktype = self.link_type[link-1] title = self.link_title[link-1] title = title.rstrip(b'. ').decode('utf-8') alt_title = parares[lstart:] alt_title = alt_title.strip() # now strip off the actual printed page number alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.') alt_title = alt_title.rstrip('. ') # skip over any external links - can't have them in a books toc if linktype == 'external' : title = '' alt_title = '' linkpage = '' else : if len(self.link_page) >= link : ptarget = self.link_page[link-1] - 1 linkpage = '%04d' % ptarget else : # just link to the current page linkpage = self.id[4:] if len(alt_title) >= len(title): title = alt_title if title != '' and linkpage != '': tocentry += title + '|' + linkpage + '\n' lstart = len(parares) if word == '_link_' : word = '' elif (link < 0) : if word == '_link_' : word = '' if word == '_lb_': word = '' sep = '' if num in self.dehyphen_rootid : word = word[0:-1] sep = '' parares += word + sep else : continue return tocentry # walk the document tree collecting the information needed # to build an html page using the ocrText def process(self): tocinfo = '' hlst = [] # get the ocr text (pos, argres) = self.findinDoc(b'info.word.ocrText',0,-1) if argres : self.ocrtext = argres.split(b'|') # get information to dehyphenate the text self.dehyphen_rootid = self.getData(b'info.dehyphen.rootID',0,-1) # determine if first paragraph is continued from previous page (pos, self.parastems_stemid) = self.findinDoc(b'info.paraStems.stemID',0,-1) first_para_continued = (self.parastems_stemid != None) # determine if last paragraph is continued onto the next page (pos, self.paracont_stemid) = self.findinDoc(b'info.paraCont.stemID',0,-1) last_para_continued = (self.paracont_stemid != None) # collect link ids self.link_id = self.getData(b'info.word.link_id',0,-1) # collect link destination page numbers self.link_page = self.getData(b'info.links.page',0,-1) # collect link types (container versus external) (pos, argres) = self.findinDoc(b'info.links.type',0,-1) if argres : self.link_type = argres.split(b'|') # collect link destinations (pos, argres) = self.findinDoc(b'info.links.href',0,-1) if argres : self.link_href = argres.split(b'|') # collect link titles (pos, argres) = self.findinDoc(b'info.links.title',0,-1) if argres : self.link_title = argres.split(b'|') else: self.link_title.append('') # get a descriptions of the starting points of the regions # and groups on the page (pagetype, pageDesc) = self.PageDescription() regcnt = len(pageDesc) - 1 anchorSet = False breakSet = False inGroup = False # process each region on the page and convert what you can to html for j in range(regcnt): (etype, start) = pageDesc[j] (ntype, end) = pageDesc[j+1] # set anchor for link target on this page if not anchorSet and not first_para_continued: hlst.append('\n') anchorSet = True # handle groups of graphics with text captions if (etype == b'grpbeg'): (pos, grptype) = self.findinDoc(b'group.type', start, end) if grptype != None: if grptype == b'graphic': gcstr = ' class="' + grptype.decode('utf-8') + '"' hlst.append('') inGroup = True elif (etype == b'grpend'): if inGroup: hlst.append('\n') inGroup = False else: (pos, regtype) = self.findinDoc(b'region.type',start,end) if regtype == b'graphic' : (pos, simgsrc) = self.findinDoc(b'img.src',start,end) if simgsrc: if inGroup: hlst.append('' % int(simgsrc)) else: hlst.append('
' % int(simgsrc)) elif regtype == b'chapterheading' : (pclass, pdesc) = self.getParaDescription(start,end, regtype) if not breakSet: hlst.append('
 
\n') breakSet = True tag = 'h1' if pclass and (len(pclass) >= 7): if pclass[3:7] == b'ch1-' : tag = 'h1' if pclass[3:7] == b'ch2-' : tag = 'h2' if pclass[3:7] == b'ch3-' : tag = 'h3' hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">') else: hlst.append('<' + tag + '>') hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append('') elif (regtype == b'text') or (regtype == b'fixed') or (regtype == b'insert') or (regtype == b'listitem'): ptype = 'full' # check to see if this is a continution from the previous page if first_para_continued : ptype = 'end' first_para_continued = False (pclass, pdesc) = self.getParaDescription(start,end, regtype) if pclass and (len(pclass) >= 6) and (ptype == 'full'): tag = 'p' if pclass[3:6] == b'h1-' : tag = 'h4' if pclass[3:6] == b'h2-' : tag = 'h5' if pclass[3:6] == b'h3-' : tag = 'h6' hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">') hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append('') else : hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) elif (regtype == b'tocentry') : ptype = 'full' if first_para_continued : ptype = 'end' first_para_continued = False (pclass, pdesc) = self.getParaDescription(start,end, regtype) tocinfo += self.buildTOCEntry(pdesc) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) elif (regtype == b'vertical') or (regtype == b'table') : ptype = 'full' if inGroup: ptype = 'middle' if first_para_continued : ptype = 'end' first_para_continued = False (pclass, pdesc) = self.getParaDescription(start, end, regtype) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) elif (regtype == b'synth_fcvr.center'): (pos, simgsrc) = self.findinDoc(b'img.src',start,end) if simgsrc: hlst.append('
' % int(simgsrc)) else : print(' Making region type', regtype, end=' ') (pos, temp) = self.findinDoc(b'paragraph',start,end) (pos2, temp) = self.findinDoc(b'span',start,end) if pos != -1 or pos2 != -1: print(' a "text" region') orig_regtype = regtype regtype = b'fixed' ptype = 'full' # check to see if this is a continution from the previous page if first_para_continued : ptype = 'end' first_para_continued = False (pclass, pdesc) = self.getParaDescription(start,end, regtype) if not pclass: if orig_regtype.endswith(b'.right') : pclass = b'cl-right' elif orig_regtype.endswith(b'.center') : pclass = b'cl-center' elif orig_regtype.endswith(b'.left') : pclass = b'cl-left' elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify' if pclass and (ptype == 'full') and (len(pclass) >= 6): tag = 'p' if pclass[3:6] == b'h1-' : tag = 'h4' if pclass[3:6] == b'h2-' : tag = 'h5' if pclass[3:6] == b'h3-' : tag = 'h6' hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">') hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append('') else : hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) else : print(' a "graphic" region') (pos, simgsrc) = self.findinDoc(b'img.src',start,end) if simgsrc: hlst.append('
' % int(simgsrc)) htmlpage = "".join(hlst) if last_para_continued : if htmlpage[-4:] == '

': htmlpage = htmlpage[0:-4] last_para_continued = False return htmlpage, tocinfo def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage): # create a document parser dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) htmlpage, tocinfo = dp.process() return htmlpage, tocinfo ================================================ FILE: DeDRM_plugin/flatxml2svg.py ================================================ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import sys import csv import os import getopt from struct import pack from struct import unpack class PParser(object): def __init__(self, gd, flatxml, meta_array): self.gd = gd self.flatdoc = flatxml.split(b'\n') self.docSize = len(self.flatdoc) self.temp = [] self.ph = -1 self.pw = -1 startpos = self.posinDoc('page.h') or self.posinDoc('book.h') for p in startpos: (name, argres) = self.lineinDoc(p) self.ph = max(self.ph, int(argres)) startpos = self.posinDoc('page.w') or self.posinDoc('book.w') for p in startpos: (name, argres) = self.lineinDoc(p) self.pw = max(self.pw, int(argres)) if self.ph <= 0: self.ph = int(meta_array.get('pageHeight', '11000')) if self.pw <= 0: self.pw = int(meta_array.get('pageWidth', '8500')) res = [] startpos = self.posinDoc('info.glyph.x') for p in startpos: argres = self.getDataatPos('info.glyph.x', p) res.extend(argres) self.gx = res res = [] startpos = self.posinDoc('info.glyph.y') for p in startpos: argres = self.getDataatPos('info.glyph.y', p) res.extend(argres) self.gy = res res = [] startpos = self.posinDoc('info.glyph.glyphID') for p in startpos: argres = self.getDataatPos('info.glyph.glyphID', p) res.extend(argres) self.gid = res # return tag at line pos in document def lineinDoc(self, pos) : if (pos >= 0) and (pos < self.docSize) : item = self.flatdoc[pos] if item.find(b'=') >= 0: (name, argres) = item.split(b'=',1) else : name = item argres = b'' return name, argres # find tag in doc if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None if end == -1 : end = self.docSize else: end = min(self.docSize, end) foundat = -1 for j in range(pos, end): item = self.flatdoc[j] if item.find(b'=') >= 0: (name, argres) = item.split(b'=',1) else : name = item argres = b'' if (isinstance(tagpath,str)): tagpath = tagpath.encode('utf-8') if name.endswith(tagpath) : result = argres foundat = j break return foundat, result # return list of start positions for the tagpath def posinDoc(self, tagpath): startpos = [] pos = 0 res = "" while res != None : (foundpos, res) = self.findinDoc(tagpath, pos, -1) if res != None : startpos.append(foundpos) pos = foundpos + 1 return startpos def getData(self, path): result = None cnt = len(self.flatdoc) for j in range(cnt): item = self.flatdoc[j] if item.find(b'=') >= 0: (name, argt) = item.split(b'=') argres = argt.split(b'|') else: name = item argres = [] if (name.endswith(path)): result = argres break if (len(argres) > 0) : for j in range(0,len(argres)): argres[j] = int(argres[j]) return result def getDataatPos(self, path, pos): result = None item = self.flatdoc[pos] if item.find(b'=') >= 0: (name, argt) = item.split(b'=') argres = argt.split(b'|') else: name = item argres = [] if (len(argres) > 0) : for j in range(0,len(argres)): argres[j] = int(argres[j]) if (isinstance(path,str)): path = path.encode('utf-8') if (name.endswith(path)): result = argres return result def getDataTemp(self, path): result = None cnt = len(self.temp) for j in range(cnt): item = self.temp[j] if item.find(b'=') >= 0: (name, argt) = item.split(b'=') argres = argt.split(b'|') else: name = item argres = [] if (isinstance(path,str)): path = path.encode('utf-8') if (name.endswith(path)): result = argres self.temp.pop(j) break if (len(argres) > 0) : for j in range(0,len(argres)): argres[j] = int(argres[j]) return result def getImages(self): result = [] self.temp = self.flatdoc while (self.getDataTemp('img') != None): h = self.getDataTemp('img.h')[0] w = self.getDataTemp('img.w')[0] x = self.getDataTemp('img.x')[0] y = self.getDataTemp('img.y')[0] src = self.getDataTemp('img.src')[0] result.append('\n' % (src, x, y, w, h)) return result def getGlyphs(self): result = [] if (self.gid != None) and (len(self.gid) > 0): glyphs = [] for j in set(self.gid): glyphs.append(j) glyphs.sort() for gid in glyphs: id='id="gl%d"' % gid path = self.gd.lookup(id) if path: result.append(id + ' ' + path) return result def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi): mlst = [] pp = PParser(gdict, flat_xml, meta_array) mlst.append('\n') if (raw): mlst.append('\n') mlst.append('\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1)) mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) else: mlst.append('\n') mlst.append('\n') mlst.append('Page %d - %s by %s\n' % (pageid, meta_array['Title'],meta_array['Authors'])) mlst.append('\n') mlst.append('\n') mlst.append('\n') mlst.append('\n') mlst.append('\n') mlst.append('\n') mlst.append('\n') return "".join(mlst) ================================================ FILE: DeDRM_plugin/genbook.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # Python 3 for calibre 5.0 from __future__ import print_function # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) import sys import csv import os import getopt from struct import pack from struct import unpack class TpzDRMError(Exception): pass # local support routines if 'calibre' in sys.modules: inCalibre = True else: inCalibre = False if inCalibre : from calibre_plugins.dedrm import convert2xml from calibre_plugins.dedrm import flatxml2html from calibre_plugins.dedrm import flatxml2svg from calibre_plugins.dedrm import stylexml2css else : import convert2xml import flatxml2html import flatxml2svg import stylexml2css # global switch buildXML = False # Get a 7 bit encoded number from a file def readEncodedNumber(file): flag = False c = file.read(1) if (len(c) == 0): return None data = ord(c) if data == 0xFF: flag = True c = file.read(1) if (len(c) == 0): return None data = ord(c) if data >= 0x80: datax = (data & 0x7F) while data >= 0x80 : c = file.read(1) if (len(c) == 0): return None data = ord(c) datax = (datax <<7) + (data & 0x7F) data = datax if flag: data = -data return data # Get a length prefixed string from the file def lengthPrefixString(data): return encodeNumber(len(data))+data def readString(file): stringLength = readEncodedNumber(file) if (stringLength == None): return None sv = file.read(stringLength) if (len(sv) != stringLength): return "" return unpack(str(stringLength)+"s",sv)[0] def getMetaArray(metaFile): # parse the meta file result = {} fo = open(metaFile,'rb') size = readEncodedNumber(fo) for i in range(size): tag = readString(fo) value = readString(fo) result[tag] = value # print(tag, value) fo.close() return result # dictionary of all text strings by index value class Dictionary(object): def __init__(self, dictFile): self.filename = dictFile self.size = 0 self.fo = open(dictFile,'rb') self.stable = [] self.size = readEncodedNumber(self.fo) for i in range(self.size): self.stable.append(self.escapestr(readString(self.fo))) self.pos = 0 def escapestr(self, str): str = str.replace(b'&',b'&') str = str.replace(b'<',b'<') str = str.replace(b'>',b'>') str = str.replace(b'=',b'=') return str def lookup(self,val): if ((val >= 0) and (val < self.size)) : self.pos = val return self.stable[self.pos] else: print("Error: %d outside of string table limits" % val) raise TpzDRMError('outside or string table limits') # sys.exit(-1) def getSize(self): return self.size def getPos(self): return self.pos class PageDimParser(object): def __init__(self, flatxml): self.flatdoc = flatxml.split(b'\n') # find tag if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None docList = self.flatdoc cnt = len(docList) if end == -1 : end = cnt else: end = min(cnt,end) foundat = -1 for j in range(pos, end): item = docList[j] if item.find(b'=') >= 0: (name, argres) = item.split(b'=') else : name = item argres = '' if name.endswith(tagpath) : result = argres foundat = j break return foundat, result def process(self): (pos, sph) = self.findinDoc(b'page.h',0,-1) (pos, spw) = self.findinDoc(b'page.w',0,-1) if (sph == None): sph = '-1' if (spw == None): spw = '-1' return sph, spw def getPageDim(flatxml): # create a document parser dp = PageDimParser(flatxml) (ph, pw) = dp.process() return ph, pw class GParser(object): def __init__(self, flatxml): self.flatdoc = flatxml.split(b'\n') self.dpi = 1440 self.gh = self.getData(b'info.glyph.h') self.gw = self.getData(b'info.glyph.w') self.guse = self.getData(b'info.glyph.use') if self.guse : self.count = len(self.guse) else : self.count = 0 self.gvtx = self.getData(b'info.glyph.vtx') self.glen = self.getData(b'info.glyph.len') self.gdpi = self.getData(b'info.glyph.dpi') self.vx = self.getData(b'info.vtx.x') self.vy = self.getData(b'info.vtx.y') self.vlen = self.getData(b'info.len.n') if self.vlen : self.glen.append(len(self.vlen)) elif self.glen: self.glen.append(0) if self.vx : self.gvtx.append(len(self.vx)) elif self.gvtx : self.gvtx.append(0) def getData(self, path): result = None cnt = len(self.flatdoc) for j in range(cnt): item = self.flatdoc[j] if item.find(b'=') >= 0: (name, argt) = item.split(b'=') argres = argt.split(b'|') else: name = item argres = [] if (name == path): result = argres break if (len(argres) > 0) : for j in range(0,len(argres)): argres[j] = int(argres[j]) return result def getGlyphDim(self, gly): if self.gdpi[gly] == 0: return 0, 0 maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly] maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly] return maxh, maxw def getPath(self, gly): path = '' if (gly < 0) or (gly >= self.count): return path tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] p = 0 for k in range(self.glen[gly], self.glen[gly+1]): if (p == 0): zx = tx[0:self.vlen[k]+1] zy = ty[0:self.vlen[k]+1] else: zx = tx[self.vlen[k-1]+1:self.vlen[k]+1] zy = ty[self.vlen[k-1]+1:self.vlen[k]+1] p += 1 j = 0 while ( j < len(zx) ): if (j == 0): # Start Position. path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly]) elif (j <= len(zx)-3): # Cubic Bezier Curve path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly]) j += 2 elif (j == len(zx)-2): # Cubic Bezier Curve to Start Position path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) j += 1 elif (j == len(zx)-1): # Quadratic Bezier Curve to Start Position path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) j += 1 path += 'z' return path # dictionary of all text strings by index value class GlyphDict(object): def __init__(self): self.gdict = {} def lookup(self, id): # id='id="gl%d"' % val if id in self.gdict: return self.gdict[id] return None def addGlyph(self, val, path): id='id="gl%d"' % val self.gdict[id] = path def generateBook(bookDir, raw, fixedimage): # sanity check Topaz file extraction if not os.path.exists(bookDir) : print("Can not find directory with unencrypted book") return 1 dictFile = os.path.join(bookDir,'dict0000.dat') if not os.path.exists(dictFile) : print("Can not find dict0000.dat file") return 1 pageDir = os.path.join(bookDir,'page') if not os.path.exists(pageDir) : print("Can not find page directory in unencrypted book") return 1 imgDir = os.path.join(bookDir,'img') if not os.path.exists(imgDir) : print("Can not find image directory in unencrypted book") return 1 glyphsDir = os.path.join(bookDir,'glyphs') if not os.path.exists(glyphsDir) : print("Can not find glyphs directory in unencrypted book") return 1 metaFile = os.path.join(bookDir,'metadata0000.dat') if not os.path.exists(metaFile) : print("Can not find metadata0000.dat in unencrypted book") return 1 svgDir = os.path.join(bookDir,'svg') if not os.path.exists(svgDir) : os.makedirs(svgDir) if buildXML: xmlDir = os.path.join(bookDir,'xml') if not os.path.exists(xmlDir) : os.makedirs(xmlDir) otherFile = os.path.join(bookDir,'other0000.dat') if not os.path.exists(otherFile) : print("Can not find other0000.dat in unencrypted book") return 1 print("Updating to color images if available") spath = os.path.join(bookDir,'color_img') dpath = os.path.join(bookDir,'img') filenames = os.listdir(spath) filenames = sorted(filenames) for filename in filenames: imgname = filename.replace('color','img') sfile = os.path.join(spath,filename) dfile = os.path.join(dpath,imgname) imgdata = open(sfile,'rb').read() open(dfile,'wb').write(imgdata) print("Creating cover.jpg") isCover = False cpath = os.path.join(bookDir,'img') cpath = os.path.join(cpath,'img0000.jpg') if os.path.isfile(cpath): cover = open(cpath, 'rb').read() cpath = os.path.join(bookDir,'cover.jpg') open(cpath, 'wb').write(cover) isCover = True print('Processing Dictionary') dict = Dictionary(dictFile) print('Processing Meta Data and creating OPF') meta_array = getMetaArray(metaFile) # replace special chars in title and authors like & < > title = meta_array.get('Title','No Title Provided') title = title.replace('&','&') title = title.replace('<','<') title = title.replace('>','>') meta_array['Title'] = title authors = meta_array.get('Authors','No Authors Provided') authors = authors.replace('&','&') authors = authors.replace('<','<') authors = authors.replace('>','>') meta_array['Authors'] = authors if buildXML: xname = os.path.join(xmlDir, 'metadata.xml') mlst = [] for key in meta_array: mlst.append('\n') metastr = "".join(mlst) mlst = None open(xname, 'wb').write(metastr) print('Processing StyleSheet') # get some scaling info from metadata to use while processing styles # and first page info fontsize = '135' if 'fontSize' in meta_array: fontsize = meta_array['fontSize'] # also get the size of a normal text page # get the total number of pages unpacked as a safety check filenames = os.listdir(pageDir) numfiles = len(filenames) spage = '1' if 'firstTextPage' in meta_array: spage = meta_array['firstTextPage'] pnum = int(spage) if pnum >= numfiles or pnum < 0: # metadata is wrong so just select a page near the front # 10% of the book to get a normal text page pnum = int(0.10 * numfiles) # print "first normal text page is", spage # get page height and width from first text page for use in stylesheet scaling pname = 'page%04d.dat' % (pnum - 1) fname = os.path.join(pageDir,pname) flat_xml = convert2xml.fromData(dict, fname) (ph, pw) = getPageDim(flat_xml) if (ph == '-1') or (ph == '0') : ph = '11000' if (pw == '-1') or (pw == '0') : pw = '8500' meta_array['pageHeight'] = ph meta_array['pageWidth'] = pw if 'fontSize' not in meta_array.keys(): meta_array['fontSize'] = fontsize # process other.dat for css info and for map of page files to svg images # this map is needed because some pages actually are made up of multiple # pageXXXX.xml files xname = os.path.join(bookDir, 'style.css') flat_xml = convert2xml.fromData(dict, otherFile) # extract info.original.pid to get original page information pageIDMap = {} pageidnums = stylexml2css.getpageIDMap(flat_xml) if len(pageidnums) == 0: filenames = os.listdir(pageDir) numfiles = len(filenames) for k in range(numfiles): pageidnums.append(k) # create a map from page ids to list of page file nums to process for that page for i in range(len(pageidnums)): id = pageidnums[i] if id in pageIDMap.keys(): pageIDMap[id].append(i) else: pageIDMap[id] = [i] # now get the css info cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) open(xname, 'w').write(cssstr) if buildXML: xname = os.path.join(xmlDir, 'other0000.xml') open(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) print('Processing Glyphs') gd = GlyphDict() filenames = os.listdir(glyphsDir) filenames = sorted(filenames) glyfname = os.path.join(svgDir,'glyphs.svg') glyfile = open(glyfname, 'w') glyfile.write('\n') glyfile.write('\n') glyfile.write('\n') glyfile.write('Glyphs for %s\n' % meta_array['Title']) glyfile.write('\n') counter = 0 for filename in filenames: # print ' ', filename print('.', end=' ') fname = os.path.join(glyphsDir,filename) flat_xml = convert2xml.fromData(dict, fname) if buildXML: xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) open(xname, 'wb').write(convert2xml.getXML(dict, fname)) gp = GParser(flat_xml) for i in range(0, gp.count): path = gp.getPath(i) maxh, maxw = gp.getGlyphDim(i) fullpath = '\n' % (counter * 256 + i, path, maxw, maxh) glyfile.write(fullpath) gd.addGlyph(counter * 256 + i, fullpath) counter += 1 glyfile.write('\n') glyfile.write('\n') glyfile.close() print(" ") # start up the html # also build up tocentries while processing html htmlFileName = "book.html" hlst = [] hlst.append('\n') hlst.append('\n') hlst.append('\n') hlst.append('\n') hlst.append('\n') hlst.append('' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n') hlst.append('\n') hlst.append('\n') if 'ASIN' in meta_array: hlst.append('\n') if 'GUID' in meta_array: hlst.append('\n') hlst.append('\n') hlst.append('\n\n') print('Processing Pages') # Books are at 1440 DPI. This is rendering at twice that size for # readability when rendering to the screen. scaledpi = 1440.0 filenames = os.listdir(pageDir) filenames = sorted(filenames) numfiles = len(filenames) xmllst = [] elst = [] for filename in filenames: # print ' ', filename print(".", end=' ') fname = os.path.join(pageDir,filename) flat_xml = convert2xml.fromData(dict, fname) # keep flat_xml for later svg processing xmllst.append(flat_xml) if buildXML: xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) open(xname, 'wb').write(convert2xml.getXML(dict, fname)) # first get the html pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) elst.append(tocinfo) hlst.append(pagehtml) # finish up the html string and output it hlst.append('\n\n') htmlstr = "".join(hlst) hlst = None open(os.path.join(bookDir, htmlFileName), 'w').write(htmlstr) print(" ") print('Extracting Table of Contents from Amazon OCR') # first create a table of contents file for the svg images tlst = [] tlst.append('\n') tlst.append('\n') tlst.append('') tlst.append('\n') tlst.append('' + meta_array['Title'] + '\n') tlst.append('\n') tlst.append('\n') if 'ASIN' in meta_array: tlst.append('\n') if 'GUID' in meta_array: tlst.append('\n') tlst.append('\n') tlst.append('\n') tlst.append('

Table of Contents

\n') start = pageidnums[0] if (raw): startname = 'page%04d.svg' % start else: startname = 'page%04d.xhtml' % start tlst.append('

Start of Book

\n') # build up a table of contents for the svg xhtml output tocentries = "".join(elst) elst = None toclst = tocentries.split('\n') toclst.pop() for entry in toclst: print(entry) title, pagenum = entry.split('|') id = pageidnums[int(pagenum)] if (raw): fname = 'page%04d.svg' % id else: fname = 'page%04d.xhtml' % id tlst.append('

' + title + '

\n') tlst.append('\n') tlst.append('\n') tochtml = "".join(tlst) open(os.path.join(svgDir, 'toc.xhtml'), 'w').write(tochtml) # now create index_svg.xhtml that points to all required files slst = [] slst.append('\n') slst.append('\n') slst.append('') slst.append('\n') slst.append('' + meta_array['Title'] + '\n') slst.append('\n') slst.append('\n') if 'ASIN' in meta_array: slst.append('\n') if 'GUID' in meta_array: slst.append('\n') slst.append('\n') slst.append('\n') print("Building svg images of each book page") slst.append('

List of Pages

\n') slst.append('
\n') idlst = sorted(pageIDMap.keys()) numids = len(idlst) cnt = len(idlst) previd = None for j in range(cnt): pageid = idlst[j] if j < cnt - 1: nextid = idlst[j+1] else: nextid = None print('.', end=' ') pagelst = pageIDMap[pageid] flst = [] for page in pagelst: flst.append(xmllst[page]) flat_svg = b"".join(flst) flst=None svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi) if (raw) : pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w') slst.append('Page %d\n' % (pageid, pageid)) else : pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w') slst.append('Page %d\n' % (pageid, pageid)) previd = pageid pfile.write(svgxml) pfile.close() counter += 1 slst.append('
\n') slst.append('

Table of Contents

\n') slst.append('\n\n') svgindex = "".join(slst) slst = None open(os.path.join(bookDir, 'index_svg.xhtml'), 'w').write(svgindex) print(" ") # build the opf file opfname = os.path.join(bookDir, 'book.opf') olst = [] olst.append('\n') olst.append('\n') # adding metadata olst.append(' \n') if b'GUID' in meta_array: olst.append(' ' + meta_array[b'GUID'].decode('utf-8') + '\n') if b'ASIN' in meta_array: olst.append(' ' + meta_array[b'ASIN'].decode('utf-8') + '\n') if b'oASIN' in meta_array: olst.append(' ' + meta_array[b'oASIN'].decode('utf-8') + '\n') olst.append(' ' + meta_array[b'Title'].decode('utf-8') + '\n') olst.append(' ' + meta_array[b'Authors'].decode('utf-8') + '\n') olst.append(' en\n') olst.append(' ' + meta_array[b'UpdateTime'].decode('utf-8') + '\n') if isCover: olst.append(' \n') olst.append(' \n') olst.append('\n') olst.append(' \n') olst.append(' \n') # adding image files to manifest filenames = os.listdir(imgDir) filenames = sorted(filenames) for filename in filenames: imgname, imgext = os.path.splitext(filename) if imgext == '.jpg': imgext = 'jpeg' if imgext == '.svg': imgext = 'svg+xml' olst.append(' \n') if isCover: olst.append(' \n') olst.append('\n') # adding spine olst.append('\n \n\n') if isCover: olst.append(' \n') olst.append(' \n') olst.append(' \n') olst.append('\n') opfstr = "".join(olst) olst = None open(opfname, 'w').write(opfstr) print('Processing Complete') return 0 def usage(): print("genbook.py generates a book from the extract Topaz Files") print("Usage:") print(" genbook.py [-r] [-h [--fixed-image] ") print(" ") print("Options:") print(" -h : help - print this usage message") print(" -r : generate raw svg files (not wrapped in xhtml)") print(" --fixed-image : genearate any Fixed Area as an svg image in the html") print(" ") def main(argv): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) bookDir = '' if len(argv) == 0: argv = sys.argv try: opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) except getopt.GetoptError as err: print(str(err)) usage() return 1 if len(opts) == 0 and len(args) == 0 : usage() return 1 raw = 0 fixedimage = True for o, a in opts: if o =="-h": usage() return 0 if o =="-r": raw = 1 if o =="--fixed-image": fixedimage = True bookDir = args[0] rv = generateBook(bookDir, raw, fixedimage) return rv if __name__ == '__main__': sys.exit(main('')) ================================================ FILE: DeDRM_plugin/ignobleepub.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ignobleepub.py # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # # Revision history: # 1 - Initial release # 2 - Added OS X support by using OpenSSL when available # 3 - screen out improper key lengths to prevent segfaults on Linux # 3.1 - Allow Windows versions of libcrypto to be found # 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml # 3.3 - On Windows try PyCrypto first, OpenSSL next # 3.4 - Modify interface to allow use with import # 3.5 - Fix for potential problem with PyCrypto # 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code # 3.7 - Tweaked to match ineptepub more closely # 3.8 - Fixed to retain zip file metadata (e.g. file modification date) # 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility # 4.0 - Work if TkInter is missing # 4.1 - Import tkFileDialog, don't assume something else will import it. # 5.0 - Python 3 for calibre 5.0 """ Decrypt Barnes & Noble encrypted ePub books. """ __license__ = 'GPL v3' __version__ = "5.0" import sys import os import traceback import base64 import zlib import zipfile from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from contextlib import closing import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data,str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] return ["ineptepub.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class IGNOBLEError(Exception): pass def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class AES(object): def __init__(self, userkey): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise IGNOBLEError('AES improper key used') return key = self._key = AES_KEY() rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: raise IGNOBLEError('Failed to initialize AES key') def decrypt(self, data): out = create_string_buffer(len(data)) iv = (b'\x00' * self._blocksize) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: raise IGNOBLEError('AES decryption failed') return out.raw return AES def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES class AES(object): def __init__(self, key): self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16) def decrypt(self, data): return self._aes.decrypt(data) return AES def _load_crypto(): AES = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: AES = loader() break except (ImportError, IGNOBLEError): pass return AES AES = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), enc('CipherReference')) for elem in encryption.findall(expr): path = elem.get('URI', None) if path is not None: path = path.encode('utf-8') encrypted.add(path) def decompress(self, bytes): dc = zlib.decompressobj(-15) bytes = dc.decompress(bytes) ex = dc.decompress(b'Z') + dc.flush() if ex: bytes = bytes + ex return bytes def decrypt(self, path, data): if bytes(path,'utf-8') in self._encrypted: data = self._aes.decrypt(data)[16:] data = data[:-data[-1]] data = self.decompress(data) return data # check file to make check whether it's probably an Adobe Adept encrypted ePub def ignobleBook(inpath): with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: return False try: rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) == 64: return True except: # if we couldn't check, assume it is return True return False def decryptBook(keyb64, inpath, outpath): if AES is None: raise IGNOBLEError("PyCrypto or OpenSSL must be installed.") key = base64.b64decode(keyb64)[:16] aes = AES(key) with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: print("{0:s} is DRM-free.".format(os.path.basename(inpath))) return 1 for name in META_NAMES: namelist.remove(name) try: rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) != 64: print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath))) return 1 bookkey = aes.decrypt(base64.b64decode(bookkey)) bookkey = bookkey[:-bookkey[-1]] encryption = inf.read('META-INF/encryption.xml') decryptor = Decryptor(bookkey[-16:], encryption) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: zi = ZipInfo('mimetype') zi.compress_type=ZIP_STORED try: # if the mimetype is present, get its info, including time-stamp oldzi = inf.getinfo('mimetype') # copy across fields to be preserved zi.date_time = oldzi.date_time zi.comment = oldzi.comment zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr zi.create_system = oldzi.create_system except: pass outf.writestr(zi, inf.read('mimetype')) for path in namelist: data = inf.read(path) zi = ZipInfo(path) zi.compress_type=ZIP_DEFLATED try: # get the file info, including time-stamp oldzi = inf.getinfo(path) # copy across useful fields zi.date_time = oldzi.date_time zi.comment = oldzi.comment zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr zi.create_system = oldzi.create_system except: pass outf.writestr(zi, decryptor.decrypt(path, data)) except: print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) return 2 return 0 def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) if result == 0: print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) return result def gui_main(): try: import tkinter import tkinter.constants import tkinter.filedialog import tkinter.messagebox import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Select files for decryption") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Key file").grid(row=0) self.keypath = tkinter.Entry(body, width=30) self.keypath.grid(row=0, column=1, sticky=sticky) if os.path.exists("bnepubkey.b64"): self.keypath.insert(0, "bnepubkey.b64") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) tkinter.Label(body, text="Input file").grid(row=1) self.inpath = tkinter.Entry(body, width=30) self.inpath.grid(row=1, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_inpath) button.grid(row=1, column=2) tkinter.Label(body, text="Output file").grid(row=2) self.outpath = tkinter.Entry(body, width=30) self.outpath.grid(row=2, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Decrypt", width=10, command=self.decrypt) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.askopenfilename( parent=None, title="Select Barnes & Noble \'.b64\' key file", defaultextension=".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def get_inpath(self): inpath = tkinter.filedialog.askopenfilename( parent=None, title="Select B&N-encrypted ePub file to decrypt", defaultextension=".epub", filetypes=[('ePub files', '.epub')]) if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, tkinter.constants.END) self.inpath.insert(0, inpath) return def get_outpath(self): outpath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select unencrypted ePub file to produce", defaultextension=".epub", filetypes=[('ePub files', '.epub')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, tkinter.constants.END) self.outpath.insert(0, outpath) return def decrypt(self): keypath = self.keypath.get() inpath = self.inpath.get() outpath = self.outpath.get() if not keypath or not os.path.exists(keypath): self.status['text'] = "Specified key file does not exist" return if not inpath or not os.path.exists(inpath): self.status['text'] = "Specified input file does not exist" return if not outpath: self.status['text'] = "Output file not specified" return if inpath == outpath: self.status['text'] = "Must have different input and output files" return userkey = open(keypath,'rb').read() self.status['text'] = "Decrypting..." try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception as e: self.status['text'] = "Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = "File successfully decrypted" else: self.status['text'] = "The was an error decrypting the file." root = tkinter.Tk() root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ignoblekey.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ignoblekey.py # Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al. # Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf # Released under the terms of the GNU General Public Licence, version 3 # # Revision history: # 1.0 - Initial release # 1.1 - remove duplicates and return last key as single key # 2.0 - Python 3 for calibre 5.0 """ Get Barnes & Noble EPUB user key from nook Studio log file """ __license__ = 'GPL v3' __version__ = "2.0" import sys import os import hashlib import getopt import re # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["ignoblekey.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class DrmException(Exception): pass # Locate all of the nookStudy/nook for PC/Mac log file and return as list def getNookLogFiles(): logFiles = [] found = False if iswindows: import winreg # some 64 bit machines do not have the proper registry key for some reason # or the python interface to the 32 vs 64 bit registry is broken paths = set() if 'LOCALAPPDATA' in os.environ.keys(): # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%") if os.path.isdir(path): paths.add(path) if 'USERPROFILE' in os.environ.keys(): # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local" if os.path.isdir(path): paths.add(path) path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming" if os.path.isdir(path): paths.add(path) # User Shell Folders show take precedent over Shell Folders if present try: regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if os.path.isdir(path): paths.add(path) except WindowsError: pass try: regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") path = winreg.QueryValueEx(regkey, 'AppData')[0] if os.path.isdir(path): paths.add(path) except WindowsError: pass try: regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if os.path.isdir(path): paths.add(path) except WindowsError: pass try: regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") path = winreg.QueryValueEx(regkey, 'AppData')[0] if os.path.isdir(path): paths.add(path) except WindowsError: pass for path in paths: # look for nookStudy log file logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt' if os.path.isfile(logpath): found = True print('Found nookStudy log file: ' + logpath.encode('ascii','ignore')) logFiles.append(logpath) else: home = os.getenv('HOME') # check for BNClientLog.txt in various locations testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt' if os.path.isfile(testpath): logFiles.append(testpath) print('Found nookStudy log file: ' + testpath) found = True testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt' if os.path.isfile(testpath): logFiles.append(testpath) print('Found nookStudy log file: ' + testpath) found = True testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt' if os.path.isfile(testpath): logFiles.append(testpath) print('Found nookStudy log file: ' + testpath) found = True testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt' if os.path.isfile(testpath): logFiles.append(testpath) print('Found nookStudy log file: ' + testpath) found = True if not found: print('No nook Study log files have been found.') return logFiles # Extract CCHash key(s) from log file def getKeysFromLog(kLogFile): keys = [] regex = re.compile("ccHash: \"(.{28})\""); for line in open(kLogFile): for m in regex.findall(line): keys.append(m) return keys # interface for calibre plugin def nookkeys(files = []): keys = [] if files == []: files = getNookLogFiles() for file in files: fileKeys = getKeysFromLog(file) if fileKeys: print("Found {0} keys in the Nook Study log files".format(len(fileKeys))) keys.extend(fileKeys) return list(set(keys)) # interface for Python DeDRM # returns single key or multiple keys, depending on path or file passed in def getkey(outpath, files=[]): keys = nookkeys(files) if len(keys) > 0: if not os.path.isdir(outpath): outfile = outpath with open(outfile, 'w') as keyfileout: keyfileout.write(keys[-1]) print("Saved a key to {0}".format(outfile)) else: keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(outpath,"nookkey{0:d}.b64".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'w') as keyfileout: keyfileout.write(key) print("Saved a key to {0}".format(outfile)) return True return False def usage(progname): print("Finds the nook Study encryption keys.") print("Keys are saved to the current directory, or a specified output directory.") print("If a file name is passed instead of a directory, only the first key is saved, in that file.") print("Usage:") print(" {0:s} [-h] [-k ] []".format(progname)) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)) try: opts, args = getopt.getopt(argv[1:], "hk:") except getopt.GetoptError as err: print("Error in options or arguments: {0}".format(err.args[0])) usage(progname) sys.exit(2) files = [] for o, a in opts: if o == "-h": usage(progname) sys.exit(0) if o == "-k": files = [a] if len(args) > 1: usage(progname) sys.exit(2) if len(args) == 1: # save to the specified file or directory outpath = args[0] if not os.path.isabs(outpath): outpath = os.path.abspath(outpath) else: # save to the same directory as the script outpath = os.path.dirname(argv[0]) # make sure the outpath is the outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files): print("Could not retrieve nook Study key.") return 0 def gui_main(): try: import tkinter import tkinter.constants import tkinter.messagebox import traceback except: return cli_main() class ExceptionDialog(tkinter.Frame): def __init__(self, root, text): tkinter.Frame.__init__(self, root, border=5) label = tkinter.Label(self, text="Unexpected error:", anchor=tkinter.constants.W, justify=tkinter.constants.LEFT) label.pack(fill=tkinter.constants.X, expand=0) self.text = tkinter.Text(self) self.text.pack(fill=tkinter.constants.BOTH, expand=1) self.text.insert(tkinter.constants.END, text) argv=unicode_argv() root = tkinter.Tk() root.withdraw() progpath, progname = os.path.split(argv[0]) success = False try: keys = nookkeys() keycount = 0 for key in keys: print(key) while True: keycount += 1 outfile = os.path.join(progpath,"nookkey{0:d}.b64".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'w') as keyfileout: keyfileout.write(key) success = True tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile)) except DrmException as e: tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e))) except Exception: root.wm_state('normal') root.title(progname) text = traceback.format_exc() ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1) root.mainloop() if not success: return 1 return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ignoblekeyfetch.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ignoblekeyfetch.py # Copyright © 2015-2020 Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Based on discoveries by "Nobody You Know" # Code partly based on ignoblekeygen.py by several people. # Windows users: Before running this program, you must first install Python. # We recommend ActiveState Python 2.7.X for Windows from # http://www.activestate.com/activepython/downloads. # Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it. # # Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this # program from the command line (python ignoblekeyfetch.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1.0 - Initial version # 1.1 - Try second URL if first one fails # 2.0 - Python 3 for calibre 5.0 """ Fetch Barnes & Noble EPUB user key from B&N servers using email and password """ __license__ = 'GPL v3' __version__ = "2.0" import sys import os # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["ignoblekeyfetch.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class IGNOBLEError(Exception): pass def fetch_key(email, password): # change email and password to utf-8 if unicode if type(email)==str: email = email.encode('utf-8') if type(password)==str: password = password.encode('utf-8') import random random = "%030x" % random.randrange(16**30) import urllib.parse, urllib.request, re # try the URL from nook for PC fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" fetch_url += urllib.parse.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress=" fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" #print fetch_url found = '' try: response = urllib.request.urlopen(fetch_url) the_page = response.read() #print the_page found = re.search('ccHash>(.+?)(.+?) ".format(progname)) return 1 email, password, keypath = argv[1:] userkey = fetch_key(email, password) if len(userkey) == 28: open(keypath,'wb').write(userkey) return 0 print("Failed to fetch key.") return 1 def gui_main(): try: import tkinter import tkinter.filedialog import tkinter.constants import tkinter.messagebox import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Enter parameters") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Account email address").grid(row=0) self.name = tkinter.Entry(body, width=40) self.name.grid(row=0, column=1, sticky=sticky) tkinter.Label(body, text="Account password").grid(row=1) self.ccn = tkinter.Entry(body, width=40) self.ccn.grid(row=1, column=1, sticky=sticky) tkinter.Label(body, text="Output file").grid(row=2) self.keypath = tkinter.Entry(body, width=40) self.keypath.grid(row=2, column=1, sticky=sticky) self.keypath.insert(2, "bnepubkey.b64") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Fetch", width=10, command=self.generate) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select B&N ePub key file to produce", defaultextension=".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def generate(self): email = self.name.get() password = self.ccn.get() keypath = self.keypath.get() if not email: self.status['text'] = "Email address not given" return if not password: self.status['text'] = "Account password not given" return if not keypath: self.status['text'] = "Output keyfile path not set" return self.status['text'] = "Fetching..." try: userkey = fetch_key(email, password) except Exception as e: self.status['text'] = "Error: {0}".format(e.args[0]) return if len(userkey) == 28: open(keypath,'wb').write(userkey) self.status['text'] = "Keyfile fetched successfully" else: self.status['text'] = "Keyfile fetch failed." root = tkinter.Tk() root.title("Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ignoblekeygen.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ignoblekeygen.py # Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Windows users: Before running this program, you must first install Python. # We recommend ActiveState Python 2.7.X for Windows (x86) from # http://www.activestate.com/activepython/downloads. # You must also install PyCrypto from # http://www.voidspace.org.uk/python/modules.shtml#pycrypto # (make certain to install the version for Python 2.7). # Then save this script file as ignoblekeygen.pyw and double-click on it to run it. # # Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this # program from the command line (python ignoblekeygen.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release # 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) # 2.1 - Allow Windows versions of libcrypto to be found # 2.2 - On Windows try PyCrypto first and then OpenSSL next # 2.3 - Modify interface to allow use of import # 2.4 - Improvements to UI and now works in plugins # 2.5 - Additional improvement for unicode and plugin support # 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility # 2.7 - Work if TkInter is missing # 2.8 - Fix bug in stand-alone use (import tkFileDialog) # 3.0 - Added Python 3 compatibility for calibre 5.0 """ Generate Barnes & Noble EPUB user key from name and credit card number. """ __license__ = 'GPL v3' __version__ = "3.0" import sys import os import hashlib import base64 # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["ignoblekeygen.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class IGNOBLEError(Exception): pass def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class AES(object): def __init__(self, userkey, iv): self._blocksize = len(userkey) self._iv = iv key = self._key = AES_KEY() rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) if rv < 0: raise IGNOBLEError('Failed to initialize AES Encrypt key') def encrypt(self, data): out = create_string_buffer(len(data)) rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) if rv == 0: raise IGNOBLEError('AES encryption failed') return out.raw return AES def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES class AES(object): def __init__(self, key, iv): self._aes = _AES.new(key, _AES.MODE_CBC, iv) def encrypt(self, data): return self._aes.encrypt(data) return AES def _load_crypto(): AES = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: AES = loader() break except (ImportError, IGNOBLEError): pass return AES AES = _load_crypto() def normalize_name(name): return ''.join(x for x in name.lower() if x != ' ') def generate_key(name, ccn): # remove spaces and case from name and CC numbers. name = normalize_name(name) ccn = normalize_name(ccn) if type(name)==str: name = name.encode('utf-8') if type(ccn)==str: ccn = ccn.encode('utf-8') name = name + b'\x00' ccn = ccn + b'\x00' name_sha = hashlib.sha1(name).digest()[:16] ccn_sha = hashlib.sha1(ccn).digest()[:16] both_sha = hashlib.sha1(name + ccn).digest() aes = AES(ccn_sha, name_sha) crypt = aes.encrypt(both_sha + (b'\x0c' * 0x0c)) userkey = hashlib.sha1(crypt).digest() return base64.b64encode(userkey) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) if AES is None: print("%s: This script requires OpenSSL or PyCrypto, which must be installed " \ "separately. Read the top-of-script comment for details." % \ (progname,)) return 1 if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 name, ccn, keypath = argv[1:] userkey = generate_key(name, ccn) open(keypath,'wb').write(userkey) return 0 def gui_main(): try: import tkinter import tkinter.constants import tkinter.messagebox import tkinter.filedialog import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Enter parameters") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Account Name").grid(row=0) self.name = tkinter.Entry(body, width=40) self.name.grid(row=0, column=1, sticky=sticky) tkinter.Label(body, text="CC#").grid(row=1) self.ccn = tkinter.Entry(body, width=40) self.ccn.grid(row=1, column=1, sticky=sticky) tkinter.Label(body, text="Output file").grid(row=2) self.keypath = tkinter.Entry(body, width=40) self.keypath.grid(row=2, column=1, sticky=sticky) self.keypath.insert(2, "bnepubkey.b64") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Generate", width=10, command=self.generate) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select B&N ePub key file to produce", defaultextension=".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def generate(self): name = self.name.get() ccn = self.ccn.get() keypath = self.keypath.get() if not name: self.status['text'] = "Name not specified" return if not ccn: self.status['text'] = "Credit card number not specified" return if not keypath: self.status['text'] = "Output keyfile path not specified" return self.status['text'] = "Generating..." try: userkey = generate_key(name, ccn) except Exception as e: self.status['text'] = "Error: (0}".format(e.args[0]) return open(keypath,'wb').write(userkey) self.status['text'] = "Keyfile successfully generated" root = tkinter.Tk() if AES is None: root.withdraw() tkinter.messagebox.showerror( "Ignoble EPUB Keyfile Generator", "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ignoblepdf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ignoblepdf.py # Copyright © 2009-2020 by Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Based on version 8.0.6 of ineptpdf.py # Revision history: # 0.1 - Initial alpha testing release 2020 by Pu D. Pud # 0.2 - Python 3 for calibre 5.0 (in testing) """ Decrypts Barnes & Noble encrypted PDF files. """ __license__ = 'GPL v3' __version__ = "0.2" import sys import os import re import zlib import struct import hashlib from decimal import * from itertools import chain, islice import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] return ["ignoblepdf.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class IGNOBLEError(Exception): pass import hashlib def SHA256(message): ctx = hashlib.sha256() ctx.update(message) return ctx.digest() def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library if sys.platform.startswith('win'): libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: raise IGNOBLEError('libcrypto not found') libcrypto = CDLL(libcrypto) AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) class RC4_KEY(Structure): _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] RC4_KEY_p = POINTER(RC4_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func 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]) 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]) class ARC4(object): @classmethod def new(cls, userkey): self = ARC4() self._blocksize = len(userkey) key = self._key = RC4_KEY() RC4_set_key(key, self._blocksize, userkey) return self def __init__(self): self._blocksize = 0 self._key = None def decrypt(self, data): out = create_string_buffer(len(data)) RC4_crypt(self._key, len(data), data, out) return out.raw class AES(object): MODE_CBC = 0 @classmethod def new(cls, userkey, mode, iv): self = AES() self._blocksize = len(userkey) # mode is ignored since CBCMODE is only thing supported/used so far self._mode = mode if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise IGNOBLEError('AES improper key used') return keyctx = self._keyctx = AES_KEY() self._iv = iv rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: raise IGNOBLEError('Failed to initialize AES key') return self def __init__(self): self._blocksize = 0 self._keyctx = None self._iv = 0 self._mode = 0 def decrypt(self, data): out = create_string_buffer(len(data)) rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) if rv == 0: raise IGNOBLEError('AES decryption failed') return out.raw return (ARC4, AES) def _load_crypto_pycrypto(): from Crypto.Cipher import ARC4 as _ARC4 from Crypto.Cipher import AES as _AES class ARC4(object): @classmethod def new(cls, userkey): self = ARC4() self._arc4 = _ARC4.new(userkey) return self def __init__(self): self._arc4 = None def decrypt(self, data): return self._arc4.decrypt(data) class AES(object): MODE_CBC = _AES.MODE_CBC @classmethod def new(cls, userkey, mode, iv): self = AES() self._aes = _AES.new(userkey, mode, iv) return self def __init__(self): self._aes = None def decrypt(self, data): return self._aes.decrypt(data) return (ARC4, AES) def _load_crypto(): ARC4 = AES = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: ARC4, AES = loader() break except (ImportError, IGNOBLEError): pass return (ARC4, AES) ARC4, AES = _load_crypto() from io import BytesIO # Do we generate cross reference streams on output? # 0 = never # 1 = only if present in input # 2 = always GEN_XREF_STM = 1 # This is the value for the current document gen_xref_stm = False # will be set in PDFSerializer # PDF parsing routines from pdfminer, with changes for EBX_HANDLER # Utilities def choplist(n, seq): '''Groups every n elements of the list.''' r = [] for x in seq: r.append(x) if len(r) == n: yield tuple(r) r = [] return def nunpack(s, default=0): '''Unpacks up to 4 bytes big endian.''' l = len(s) if not l: return default elif l == 1: return ord(s) elif l == 2: return struct.unpack('>H', s)[0] elif l == 3: return struct.unpack('>L', '\x00'+s)[0] elif l == 4: return struct.unpack('>L', s)[0] else: return TypeError('invalid length: %d' % l) STRICT = 0 # PS Exceptions class PSException(Exception): pass class PSEOF(PSException): pass class PSSyntaxError(PSException): pass class PSTypeError(PSException): pass class PSValueError(PSException): pass # Basic PostScript Types # PSLiteral class PSObject(object): pass class PSLiteral(PSObject): ''' PS literals (e.g. "/Name"). Caution: Never create these objects directly. Use PSLiteralTable.intern() instead. ''' def __init__(self, name): self.name = name return def __repr__(self): name = [] for char in self.name: if not char.isalnum(): char = '#%02x' % ord(char) name.append(char) return '/%s' % ''.join(name) # PSKeyword class PSKeyword(PSObject): ''' PS keywords (e.g. "showpage"). Caution: Never create these objects directly. Use PSKeywordTable.intern() instead. ''' def __init__(self, name): self.name = name return def __repr__(self): return self.name # PSSymbolTable class PSSymbolTable(object): ''' Symbol table that stores PSLiteral or PSKeyword. ''' def __init__(self, classe): self.dic = {} self.classe = classe return def intern(self, name): if name in self.dic: lit = self.dic[name] else: lit = self.classe(name) self.dic[name] = lit return lit PSLiteralTable = PSSymbolTable(PSLiteral) PSKeywordTable = PSSymbolTable(PSKeyword) LIT = PSLiteralTable.intern KWD = PSKeywordTable.intern KEYWORD_BRACE_BEGIN = KWD('{') KEYWORD_BRACE_END = KWD('}') KEYWORD_ARRAY_BEGIN = KWD('[') KEYWORD_ARRAY_END = KWD(']') KEYWORD_DICT_BEGIN = KWD('<<') KEYWORD_DICT_END = KWD('>>') def literal_name(x): if not isinstance(x, PSLiteral): if STRICT: raise PSTypeError('Literal required: %r' % x) else: return str(x) return x.name def keyword_name(x): if not isinstance(x, PSKeyword): if STRICT: raise PSTypeError('Keyword required: %r' % x) else: return str(x) return x.name ## PSBaseParser ## EOL = re.compile(r'[\r\n]') SPC = re.compile(r'\s') NONSPC = re.compile(r'\S') HEX = re.compile(r'[0-9a-fA-F]') END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') END_NUMBER = re.compile(r'[^0-9]') END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') END_STRING = re.compile(r'[()\134]') OCT_STRING = re.compile(r'[0-7]') ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } class PSBaseParser(object): ''' Most basic PostScript parser that performs only basic tokenization. ''' BUFSIZ = 4096 def __init__(self, fp): self.fp = fp self.seek(0) return def __repr__(self): return '' % (self.fp, self.bufpos) def flush(self): return def close(self): self.flush() return def tell(self): return self.bufpos+self.charpos def poll(self, pos=None, n=80): pos0 = self.fp.tell() if not pos: pos = self.bufpos+self.charpos self.fp.seek(pos) # print('poll(%d): %r' % (pos, self.fp.read(n)), file=sys.stderr) self.fp.seek(pos0) return def seek(self, pos): ''' Seeks the parser to the given position. ''' self.fp.seek(pos) # reset the status for nextline() self.bufpos = pos self.buf = '' self.charpos = 0 # reset the status for nexttoken() self.parse1 = self.parse_main self.tokens = [] return def fillbuf(self): if self.charpos < len(self.buf): return # fetch next chunk. self.bufpos = self.fp.tell() self.buf = self.fp.read(self.BUFSIZ) if not self.buf: raise PSEOF('Unexpected EOF') self.charpos = 0 return def parse_main(self, s, i): m = NONSPC.search(s, i) if not m: return (self.parse_main, len(s)) j = m.start(0) c = s[j] self.tokenstart = self.bufpos+j if c == '%': self.token = '%' return (self.parse_comment, j+1) if c == '/': self.token = '' return (self.parse_literal, j+1) if c in '-+' or c.isdigit(): self.token = c return (self.parse_number, j+1) if c == '.': self.token = c return (self.parse_decimal, j+1) if c.isalpha(): self.token = c return (self.parse_keyword, j+1) if c == '(': self.token = '' self.paren = 1 return (self.parse_string, j+1) if c == '<': self.token = '' return (self.parse_wopen, j+1) if c == '>': self.token = '' return (self.parse_wclose, j+1) self.add_token(KWD(c)) return (self.parse_main, j+1) def add_token(self, obj): self.tokens.append((self.tokenstart, obj)) return def parse_comment(self, s, i): m = EOL.search(s, i) if not m: self.token += s[i:] return (self.parse_comment, len(s)) j = m.start(0) self.token += s[i:j] # We ignore comments. #self.tokens.append(self.token) return (self.parse_main, j) def parse_literal(self, s, i): m = END_LITERAL.search(s, i) if not m: self.token += s[i:] return (self.parse_literal, len(s)) j = m.start(0) self.token += s[i:j] c = s[j] if c == '#': self.hex = '' return (self.parse_literal_hex, j+1) self.add_token(LIT(self.token)) return (self.parse_main, j) def parse_literal_hex(self, s, i): c = s[i] if HEX.match(c) and len(self.hex) < 2: self.hex += c return (self.parse_literal_hex, i+1) if self.hex: self.token += chr(int(self.hex, 16)) return (self.parse_literal, i) def parse_number(self, s, i): m = END_NUMBER.search(s, i) if not m: self.token += s[i:] return (self.parse_number, len(s)) j = m.start(0) self.token += s[i:j] c = s[j] if c == '.': self.token += c return (self.parse_decimal, j+1) try: self.add_token(int(self.token)) except ValueError: pass return (self.parse_main, j) def parse_decimal(self, s, i): m = END_NUMBER.search(s, i) if not m: self.token += s[i:] return (self.parse_decimal, len(s)) j = m.start(0) self.token += s[i:j] self.add_token(Decimal(self.token)) return (self.parse_main, j) def parse_keyword(self, s, i): m = END_KEYWORD.search(s, i) if not m: self.token += s[i:] return (self.parse_keyword, len(s)) j = m.start(0) self.token += s[i:j] if self.token == 'true': token = True elif self.token == 'false': token = False else: token = KWD(self.token) self.add_token(token) return (self.parse_main, j) def parse_string(self, s, i): m = END_STRING.search(s, i) if not m: self.token += s[i:] return (self.parse_string, len(s)) j = m.start(0) self.token += s[i:j] c = s[j] if c == '\\': self.oct = '' return (self.parse_string_1, j+1) if c == '(': self.paren += 1 self.token += c return (self.parse_string, j+1) if c == ')': self.paren -= 1 if self.paren: self.token += c return (self.parse_string, j+1) self.add_token(self.token) return (self.parse_main, j+1) def parse_string_1(self, s, i): c = s[i] if OCT_STRING.match(c) and len(self.oct) < 3: self.oct += c return (self.parse_string_1, i+1) if self.oct: self.token += chr(int(self.oct, 8)) return (self.parse_string, i) if c in ESC_STRING: self.token += chr(ESC_STRING[c]) return (self.parse_string, i+1) def parse_wopen(self, s, i): c = s[i] if c.isspace() or HEX.match(c): return (self.parse_hexstring, i) if c == '<': self.add_token(KEYWORD_DICT_BEGIN) i += 1 return (self.parse_main, i) def parse_wclose(self, s, i): c = s[i] if c == '>': self.add_token(KEYWORD_DICT_END) i += 1 return (self.parse_main, i) def parse_hexstring(self, s, i): m = END_HEX_STRING.search(s, i) if not m: self.token += s[i:] return (self.parse_hexstring, len(s)) j = m.start(0) self.token += s[i:j] token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), SPC.sub('', self.token)) self.add_token(token) return (self.parse_main, j) def nexttoken(self): while not self.tokens: self.fillbuf() (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) token = self.tokens.pop(0) return token def nextline(self): ''' Fetches a next line that ends either with \\r or \\n. ''' linebuf = '' linepos = self.bufpos + self.charpos eol = False while 1: self.fillbuf() if eol: c = self.buf[self.charpos] # handle '\r\n' if c == '\n': linebuf += c self.charpos += 1 break m = EOL.search(self.buf, self.charpos) if m: linebuf += self.buf[self.charpos:m.end(0)] self.charpos = m.end(0) if linebuf[-1] == '\r': eol = True else: break else: linebuf += self.buf[self.charpos:] self.charpos = len(self.buf) return (linepos, linebuf) def revreadlines(self): ''' Fetches a next line backword. This is used to locate the trailers at the end of a file. ''' self.fp.seek(0, 2) pos = self.fp.tell() buf = '' while 0 < pos: prevpos = pos pos = max(0, pos-self.BUFSIZ) self.fp.seek(pos) s = self.fp.read(prevpos-pos) if not s: break while 1: n = max(s.rfind('\r'), s.rfind('\n')) if n == -1: buf = s + buf break yield s[n:]+buf s = s[:n] buf = '' return ## PSStackParser ## class PSStackParser(PSBaseParser): def __init__(self, fp): PSBaseParser.__init__(self, fp) self.reset() return def reset(self): self.context = [] self.curtype = None self.curstack = [] self.results = [] return def seek(self, pos): PSBaseParser.seek(self, pos) self.reset() return def push(self, *objs): self.curstack.extend(objs) return def pop(self, n): objs = self.curstack[-n:] self.curstack[-n:] = [] return objs def popall(self): objs = self.curstack self.curstack = [] return objs def add_results(self, *objs): self.results.extend(objs) return def start_type(self, pos, type): self.context.append((pos, self.curtype, self.curstack)) (self.curtype, self.curstack) = (type, []) return def end_type(self, type): if self.curtype != type: raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) objs = [ obj for (_,obj) in self.curstack ] (pos, self.curtype, self.curstack) = self.context.pop() return (pos, objs) def do_keyword(self, pos, token): return def nextobject(self, direct=False): ''' Yields a list of objects: keywords, literals, strings, numbers, arrays and dictionaries. Arrays and dictionaries are represented as Python sequence and dictionaries. ''' while not self.results: (pos, token) = self.nexttoken() # print((pos, token), (self.curtype, self.curstack)) if (isinstance(token, int) or isinstance(token, Decimal) or isinstance(token, bool) or isinstance(token, str) or isinstance(token, PSLiteral)): # normal token self.push((pos, token)) elif token == KEYWORD_ARRAY_BEGIN: # begin array self.start_type(pos, 'a') elif token == KEYWORD_ARRAY_END: # end array try: self.push(self.end_type('a')) except PSTypeError: if STRICT: raise elif token == KEYWORD_DICT_BEGIN: # begin dictionary self.start_type(pos, 'd') elif token == KEYWORD_DICT_END: # end dictionary try: (pos, objs) = self.end_type('d') if len(objs) % 2 != 0: print("Incomplete dictionary construct") objs.append("") # this isn't necessary. # temporary fix. is this due to rental books? # raise PSSyntaxError( # 'Invalid dictionary construct: %r' % objs) d = dict((literal_name(k), v) \ for (k,v) in choplist(2, objs)) self.push((pos, d)) except PSTypeError: if STRICT: raise else: self.do_keyword(pos, token) if self.context: continue else: if direct: return self.pop(1)[0] self.flush() obj = self.results.pop(0) return obj LITERAL_CRYPT = PSLiteralTable.intern('Crypt') LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) ## PDF Objects ## class PDFObject(PSObject): pass class PDFException(PSException): pass class PDFTypeError(PDFException): pass class PDFValueError(PDFException): pass class PDFNotImplementedError(PSException): pass ## PDFObjRef ## class PDFObjRef(PDFObject): def __init__(self, doc, objid, genno): if objid == 0: if STRICT: raise PDFValueError('PDF object id cannot be 0.') self.doc = doc self.objid = objid self.genno = genno return def __repr__(self): return '' % (self.objid, self.genno) def resolve(self): return self.doc.getobj(self.objid) # resolve def resolve1(x): ''' Resolve an object. If this is an array or dictionary, it may still contains some indirect objects inside. ''' while isinstance(x, PDFObjRef): x = x.resolve() return x def resolve_all(x): ''' Recursively resolve X and all the internals. Make sure there is no indirect reference within the nested object. This procedure might be slow. ''' while isinstance(x, PDFObjRef): x = x.resolve() if isinstance(x, list): x = [ resolve_all(v) for v in x ] elif isinstance(x, dict): for (k,v) in x.iteritems(): x[k] = resolve_all(v) return x def decipher_all(decipher, objid, genno, x): ''' Recursively decipher X. ''' if isinstance(x, str): return decipher(objid, genno, x) decf = lambda v: decipher_all(decipher, objid, genno, v) if isinstance(x, list): x = [decf(v) for v in x] elif isinstance(x, dict): x = dict((k, decf(v)) for (k, v) in x.iteritems()) return x # Type cheking def int_value(x): x = resolve1(x) if not isinstance(x, int): if STRICT: raise PDFTypeError('Integer required: %r' % x) return 0 return x def decimal_value(x): x = resolve1(x) if not isinstance(x, Decimal): if STRICT: raise PDFTypeError('Decimal required: %r' % x) return 0.0 return x def num_value(x): x = resolve1(x) if not (isinstance(x, int) or isinstance(x, Decimal)): if STRICT: raise PDFTypeError('Int or Float required: %r' % x) return 0 return x def str_value(x): x = resolve1(x) if not isinstance(x, str): if STRICT: raise PDFTypeError('String required: %r' % x) return '' return x def list_value(x): x = resolve1(x) if not (isinstance(x, list) or isinstance(x, tuple)): if STRICT: raise PDFTypeError('List required: %r' % x) return [] return x def dict_value(x): x = resolve1(x) if not isinstance(x, dict): if STRICT: raise PDFTypeError('Dict required: %r' % x) return {} return x def stream_value(x): x = resolve1(x) if not isinstance(x, PDFStream): if STRICT: raise PDFTypeError('PDFStream required: %r' % x) return PDFStream({}, '') return x # ascii85decode(data) def ascii85decode(data): n = b = 0 out = '' for c in data: if '!' <= c and c <= 'u': n += 1 b = b*85+(ord(c)-33) if n == 5: out += struct.pack('>L',b) n = b = 0 elif c == 'z': assert n == 0 out += '\0\0\0\0' elif c == '~': if n: for _ in range(5-n): b = b*85+84 out += struct.pack('>L',b)[:n-1] break return out ## PDFStream type class PDFStream(PDFObject): def __init__(self, dic, rawdata, decipher=None): length = int_value(dic.get('Length', 0)) eol = rawdata[length:] # quick and dirty fix for false length attribute, # might not work if the pdf stream parser has a problem if decipher != None and decipher.__name__ == 'decrypt_aes': if (len(rawdata) % 16) != 0: cutdiv = len(rawdata) // 16 rawdata = rawdata[:16*cutdiv] else: if eol in ('\r', '\n', '\r\n'): rawdata = rawdata[:length] self.dic = dic self.rawdata = rawdata self.decipher = decipher self.data = None self.decdata = None self.objid = None self.genno = None return def set_objid(self, objid, genno): self.objid = objid self.genno = genno return def __repr__(self): if self.rawdata: return '' % \ (self.objid, len(self.rawdata), self.dic) else: return '' % \ (self.objid, len(self.data), self.dic) def decode(self): assert self.data is None and self.rawdata is not None data = self.rawdata if self.decipher: # Handle encryption data = self.decipher(self.objid, self.genno, data) if gen_xref_stm: self.decdata = data # keep decrypted data if 'Filter' not in self.dic: self.data = data self.rawdata = None ##print(self.dict) return filters = self.dic['Filter'] if not isinstance(filters, list): filters = [ filters ] for f in filters: if f in LITERALS_FLATE_DECODE: # will get errors if the document is encrypted. data = zlib.decompress(data) elif f in LITERALS_LZW_DECODE: data = ''.join(LZWDecoder(BytesIO(data)).run()) elif f in LITERALS_ASCII85_DECODE: data = ascii85decode(data) elif f == LITERAL_CRYPT: raise PDFNotImplementedError('/Crypt filter is unsupported') else: raise PDFNotImplementedError('Unsupported filter: %r' % f) # apply predictors if 'DP' in self.dic: params = self.dic['DP'] else: params = self.dic.get('DecodeParms', {}) if 'Predictor' in params: pred = int_value(params['Predictor']) if pred: if pred != 12: raise PDFNotImplementedError( 'Unsupported predictor: %r' % pred) if 'Columns' not in params: raise PDFValueError( 'Columns undefined for predictor=12') columns = int_value(params['Columns']) buf = '' ent0 = '\x00' * columns for i in range(0, len(data), columns+1): pred = data[i] ent1 = data[i+1:i+1+columns] if pred == '\x02': ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \ for (a,b) in zip(ent0,ent1)) buf += ent1 ent0 = ent1 data = buf self.data = data self.rawdata = None return def get_data(self): if self.data is None: self.decode() return self.data def get_rawdata(self): return self.rawdata def get_decdata(self): if self.decdata is not None: return self.decdata data = self.rawdata if self.decipher and data: # Handle encryption data = self.decipher(self.objid, self.genno, data) return data ## PDF Exceptions ## class PDFSyntaxError(PDFException): pass class PDFNoValidXRef(PDFSyntaxError): pass class PDFEncryptionError(PDFException): pass class PDFPasswordIncorrect(PDFEncryptionError): pass # some predefined literals and keywords. LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm') LITERAL_XREF = PSLiteralTable.intern('XRef') LITERAL_PAGE = PSLiteralTable.intern('Page') LITERAL_PAGES = PSLiteralTable.intern('Pages') LITERAL_CATALOG = PSLiteralTable.intern('Catalog') ## XRefs ## ## PDFXRef ## class PDFXRef(object): def __init__(self): self.offsets = None return def __repr__(self): return '' % len(self.offsets) def objids(self): return self.offsets.iterkeys() def load(self, parser): self.offsets = {} while 1: try: (pos, line) = parser.nextline() except PSEOF: raise PDFNoValidXRef('Unexpected EOF - file corrupted?') if not line: raise PDFNoValidXRef('Premature eof: %r' % parser) if line.startswith('trailer'): parser.seek(pos) break f = line.strip().split(' ') if len(f) != 2: raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) try: (start, nobjs) = map(int, f) except ValueError: raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) for objid in range(start, start+nobjs): try: (_, line) = parser.nextline() except PSEOF: raise PDFNoValidXRef('Unexpected EOF - file corrupted?') f = line.strip().split(' ') if len(f) != 3: raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) (pos, genno, use) = f if use != 'n': continue self.offsets[objid] = (int(genno), int(pos)) self.load_trailer(parser) return KEYWORD_TRAILER = PSKeywordTable.intern('trailer') def load_trailer(self, parser): try: (_,kwd) = parser.nexttoken() assert kwd is self.KEYWORD_TRAILER (_,dic) = parser.nextobject(direct=True) except PSEOF: x = parser.pop(1) if not x: raise PDFNoValidXRef('Unexpected EOF - file corrupted') (_,dic) = x[0] self.trailer = dict_value(dic) return def getpos(self, objid): try: (genno, pos) = self.offsets[objid] except KeyError: raise return (None, pos) ## PDFXRefStream ## class PDFXRefStream(object): def __init__(self): self.index = None self.data = None self.entlen = None self.fl1 = self.fl2 = self.fl3 = None return def __repr__(self): return '' % self.index def objids(self): for first, size in self.index: for objid in range(first, first + size): yield objid def load(self, parser, debug=0): (_,objid) = parser.nexttoken() # ignored (_,genno) = parser.nexttoken() # ignored (_,kwd) = parser.nexttoken() (_,stream) = parser.nextobject() if not isinstance(stream, PDFStream) or \ stream.dic['Type'] is not LITERAL_XREF: raise PDFNoValidXRef('Invalid PDF stream spec.') size = stream.dic['Size'] index = stream.dic.get('Index', (0,size)) self.index = zip(islice(index, 0, None, 2), islice(index, 1, None, 2)) (self.fl1, self.fl2, self.fl3) = stream.dic['W'] self.data = stream.get_data() self.entlen = self.fl1+self.fl2+self.fl3 self.trailer = stream.dic return def getpos(self, objid): offset = 0 for first, size in self.index: if first <= objid and objid < (first + size): break offset += size else: raise KeyError(objid) i = self.entlen * ((objid - first) + offset) ent = self.data[i:i+self.entlen] f1 = nunpack(ent[:self.fl1], 1) if f1 == 1: pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) genno = nunpack(ent[self.fl1+self.fl2:]) return (None, pos) elif f1 == 2: objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) index = nunpack(ent[self.fl1+self.fl2:]) return (objid, index) # this is a free object raise KeyError(objid) ## PDFDocument ## ## A PDFDocument object represents a PDF document. ## Since a PDF file is usually pretty big, normally it is not loaded ## at once. Rather it is parsed dynamically as processing goes. ## A PDF parser is associated with the document. ## class PDFDocument(object): def __init__(self): self.xrefs = [] self.objs = {} self.parsed_objs = {} self.root = None self.catalog = None self.parser = None self.encryption = None self.decipher = None return # set_parser(parser) # Associates the document with an (already initialized) parser object. def set_parser(self, parser): if self.parser: return self.parser = parser # The document is set to be temporarily ready during collecting # all the basic information about the document, e.g. # the header, the encryption information, and the access rights # for the document. self.ready = True # Retrieve the information of each header that was appended # (maybe multiple times) at the end of the document. self.xrefs = parser.read_xref() for xref in self.xrefs: trailer = xref.trailer if not trailer: continue # If there's an encryption info, remember it. if 'Encrypt' in trailer: #assert not self.encryption try: self.encryption = (list_value(trailer['ID']), dict_value(trailer['Encrypt'])) # fix for bad files except: self.encryption = ('ffffffffffffffffffffffffffffffffffff', dict_value(trailer['Encrypt'])) if 'Root' in trailer: self.set_root(dict_value(trailer['Root'])) break else: raise PDFSyntaxError('No /Root object! - Is this really a PDF?') # The document is set to be non-ready again, until all the # proper initialization (asking the password key and # verifying the access permission, so on) is finished. self.ready = False return # set_root(root) # Set the Root dictionary of the document. # Each PDF file must have exactly one /Root dictionary. def set_root(self, root): self.root = root self.catalog = dict_value(self.root) if self.catalog.get('Type') is not LITERAL_CATALOG: if STRICT: raise PDFSyntaxError('Catalog not found!') return # initialize(password='') # Perform the initialization with a given password. # This step is mandatory even if there's no password associated # with the document. def initialize(self, password=''): if not self.encryption: self.is_printable = self.is_modifiable = self.is_extractable = True self.ready = True raise PDFEncryptionError('Document is not encrypted.') return (docid, param) = self.encryption type = literal_name(param['Filter']) if type == 'Adobe.APS': return self.initialize_adobe_ps(password, docid, param) if type == 'Standard': return self.initialize_standard(password, docid, param) if type == 'EBX_HANDLER': return self.initialize_ebx(password, docid, param) raise PDFEncryptionError('Unknown filter: param=%r' % param) def initialize_adobe_ps(self, password, docid, param): global KEYFILEPATH self.decrypt_key = self.genkey_adobe_ps(param) self.genkey = self.genkey_v4 self.decipher = self.decrypt_aes self.ready = True return def genkey_adobe_ps(self, param): # nice little offline principal keys dictionary # global static principal key for German Onleihe / Bibliothek Digital principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')} self.is_printable = self.is_modifiable = self.is_extractable = True length = int_value(param.get('Length', 0)) / 8 edcdata = str_value(param.get('EDCData')).decode('base64') pdrllic = str_value(param.get('PDRLLic')).decode('base64') pdrlpol = str_value(param.get('PDRLPol')).decode('base64') edclist = [] for pair in edcdata.split('\n'): edclist.append(pair) # principal key request for key in principalkeys: if key in pdrllic: principalkey = principalkeys[key] else: raise IGNOBLEError('Cannot find principal key for this pdf') shakey = SHA256(principalkey) ivector = 16 * chr(0) plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) if plaintext[-16:] != 16 * chr(16): raise IGNOBLEError('Offlinekey cannot be decrypted, aborting ...') pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16: raise IGNOBLEError('Could not decrypt PDRLPol, aborting ...') else: cutter = -1 * ord(pdrlpol[-1]) pdrlpol = pdrlpol[:cutter] return plaintext[:16] PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ '\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' # experimental aes pw support def initialize_standard(self, password, docid, param): # copy from a global variable 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) O = str_value(param['O']) R = int_value(param['R']) # Revision if 5 <= R: raise PDFEncryptionError('Unknown revision: %r' % R) U = str_value(param['U']) P = int_value(param['P']) try: EncMetadata = str_value(param['EncryptMetadata']) except: EncMetadata = '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 password = (password+self.PASSWORD_PADDING)[:32] # 1 hash = hashlib.md5(password) # 2 hash.update(O) # 3 hash.update(struct.pack('= 3: # Algorithm 3.5 hash = hashlib.md5(self.PASSWORD_PADDING) # 2 hash.update(docid[0]) # 3 x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 for i in range(1,19+1): k = ''.join( chr(ord(c) ^ i) for c in key ) x = ARC4.new(k).decrypt(x) u1 = x+x # 32bytes total if R == 2: is_authenticated = (u1 == U) else: is_authenticated = (u1[:16] == U[:16]) if not is_authenticated: raise IGNOBLEError('Password is not correct.') self.decrypt_key = key # genkey method if V == 1 or V == 2: self.genkey = self.genkey_v2 elif V == 3: self.genkey = self.genkey_v3 elif V == 4: self.genkey = self.genkey_v2 #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 # rc4 if V != 4: self.decipher = self.decipher_rc4 # XXX may be AES # aes elif V == 4 and Length == 128: elf.decipher = self.decipher_aes elif V == 4 and Length == 256: raise PDFNotImplementedError('AES256 encryption is currently unsupported') self.ready = True return def initialize_ebx(self, keyb64, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True key = keyb64.decode('base64')[:16] aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key)) length = int_value(param.get('Length', 0)) / 8 rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') rights = zlib.decompress(rights, -15) rights = etree.fromstring(rights) expr = './/{http://ns.adobe.com/adept}encryptedKey' bookkey = ''.join(rights.findtext(expr)).decode('base64') bookkey = aes.decrypt(bookkey) bookkey = bookkey[:-ord(bookkey[-1])] bookkey = bookkey[-16:] ebx_V = int_value(param.get('V', 4)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) # added because of improper booktype / decryption book session key errors if length > 0: if len(bookkey) == length: if ebx_V == 3: V = 3 else: V = 2 elif len(bookkey) == length + 1: V = ord(bookkey[0]) bookkey = bookkey[1:] else: print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("bookkey[0] is %d" % ord(bookkey[0])) raise IGNOBLEError('error decrypting book session key - mismatched length') else: # proper length unknown try with whatever you have print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("bookkey[0] is %d" % ord(bookkey[0])) if ebx_V == 3: V = 3 else: V = 2 self.decrypt_key = bookkey self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 self.decipher = self.decrypt_rc4 self.ready = True return # genkey functions def genkey_v2(self, objid, genno): objid = struct.pack(' PDFObjStmRef.maxindex: PDFObjStmRef.maxindex = index ## PDFParser ## class PDFParser(PSStackParser): def __init__(self, doc, fp): PSStackParser.__init__(self, fp) self.doc = doc self.doc.set_parser(self) return def __repr__(self): return '' KEYWORD_R = PSKeywordTable.intern('R') KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') KEYWORD_STREAM = PSKeywordTable.intern('stream') KEYWORD_XREF = PSKeywordTable.intern('xref') KEYWORD_STARTXREF = PSKeywordTable.intern('startxref') def do_keyword(self, pos, token): if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): self.add_results(*self.pop(1)) return if token is self.KEYWORD_ENDOBJ: self.add_results(*self.pop(4)) return if token is self.KEYWORD_R: # reference to indirect object try: ((_,objid), (_,genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass return if token is self.KEYWORD_STREAM: # stream object ((_,dic),) = self.pop(1) dic = dict_value(dic) try: objlen = int_value(dic['Length']) except KeyError: if STRICT: raise PDFSyntaxError('/Length is undefined: %r' % dic) objlen = 0 self.seek(pos) try: (_, line) = self.nextline() # 'stream' except PSEOF: if STRICT: raise PDFSyntaxError('Unexpected EOF') return pos += len(line) self.fp.seek(pos) data = self.fp.read(objlen) self.seek(pos+objlen) while 1: try: (linepos, line) = self.nextline() except PSEOF: if STRICT: raise PDFSyntaxError('Unexpected EOF') break if 'endstream' in line: i = line.index('endstream') objlen += i data += line[:i] break objlen += len(line) data += line self.seek(pos+objlen) obj = PDFStream(dic, data, self.doc.decipher) self.push((pos, obj)) return # others self.push((pos, token)) return def find_xref(self): # search the last xref table by scanning the file backwards. prev = None for line in self.revreadlines(): line = line.strip() if line == 'startxref': break if line: prev = line else: raise PDFNoValidXRef('Unexpected EOF') return int(prev) # read xref table def read_xref_from(self, start, xrefs): self.seek(start) self.reset() try: (pos, token) = self.nexttoken() except PSEOF: raise PDFNoValidXRef('Unexpected EOF') if isinstance(token, int): # XRefStream: PDF-1.5 if GEN_XREF_STM == 1: global gen_xref_stm gen_xref_stm = True self.seek(pos) self.reset() xref = PDFXRefStream() xref.load(self) else: if token is not self.KEYWORD_XREF: raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % (pos, token)) self.nextline() xref = PDFXRef() xref.load(self) xrefs.append(xref) trailer = xref.trailer if 'XRefStm' in trailer: pos = int_value(trailer['XRefStm']) self.read_xref_from(pos, xrefs) if 'Prev' in trailer: # find previous xref pos = int_value(trailer['Prev']) self.read_xref_from(pos, xrefs) return # read xref tables and trailers def read_xref(self): xrefs = [] trailerpos = None try: pos = self.find_xref() self.read_xref_from(pos, xrefs) except PDFNoValidXRef: # fallback self.seek(0) pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b') offsets = {} xref = PDFXRef() while 1: try: (pos, line) = self.nextline() except PSEOF: break if line.startswith('trailer'): trailerpos = pos # remember last trailer m = pat.match(line) if not m: continue (objid, genno) = m.groups() offsets[int(objid)] = (0, pos) if not offsets: raise xref.offsets = offsets if trailerpos: self.seek(trailerpos) xref.load_trailer(self) xrefs.append(xref) return xrefs ## PDFObjStrmParser ## class PDFObjStrmParser(PDFParser): def __init__(self, data, doc): PSStackParser.__init__(self, BytesIO(data)) self.doc = doc return def flush(self): self.add_results(*self.popall()) return KEYWORD_R = KWD('R') def do_keyword(self, pos, token): if token is self.KEYWORD_R: # reference to indirect object try: ((_,objid), (_,genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass return # others self.push((pos, token)) return ### ### My own code, for which there is none else to blame class PDFSerializer(object): def __init__(self, inf, userkey): global GEN_XREF_STM, gen_xref_stm gen_xref_stm = GEN_XREF_STM > 1 self.version = inf.read(8) inf.seek(0) self.doc = doc = PDFDocument() parser = PDFParser(doc, inf) doc.initialize(userkey) self.objids = objids = set() for xref in reversed(doc.xrefs): trailer = xref.trailer for objid in xref.objids(): objids.add(objid) trailer = dict(trailer) trailer.pop('Prev', None) trailer.pop('XRefStm', None) if 'Encrypt' in trailer: objids.remove(trailer.pop('Encrypt').objid) self.trailer = trailer def dump(self, outf): self.outf = outf self.write(self.version) self.write('\n%\xe2\xe3\xcf\xd3\n') doc = self.doc objids = self.objids xrefs = {} maxobj = max(objids) trailer = dict(self.trailer) trailer['Size'] = maxobj + 1 for objid in objids: obj = doc.getobj(objid) if isinstance(obj, PDFObjStmRef): xrefs[objid] = obj continue if obj is not None: try: genno = obj.genno except AttributeError: genno = 0 xrefs[objid] = (self.tell(), genno) self.serialize_indirect(objid, obj) startxref = self.tell() if not gen_xref_stm: self.write('xref\n') self.write('0 %d\n' % (maxobj + 1,)) for objid in range(0, maxobj + 1): if objid in xrefs: # force the genno to be 0 self.write("%010d 00000 n \n" % xrefs[objid][0]) else: self.write("%010d %05d f \n" % (0, 65535)) self.write('trailer\n') self.serialize_object(trailer) self.write('\nstartxref\n%d\n%%%%EOF' % startxref) else: # Generate crossref stream. # Calculate size of entries maxoffset = max(startxref, maxobj) maxindex = PDFObjStmRef.maxindex fl2 = 2 power = 65536 while maxoffset >= power: fl2 += 1 power *= 256 fl3 = 1 power = 256 while maxindex >= power: fl3 += 1 power *= 256 index = [] first = None prev = None data = [] # Put the xrefstream's reference in itself startxref = self.tell() maxobj += 1 xrefs[maxobj] = (startxref, 0) for objid in sorted(xrefs): if first is None: first = objid elif objid != prev + 1: index.extend((first, prev - first + 1)) first = objid prev = objid objref = xrefs[objid] if isinstance(objref, PDFObjStmRef): f1 = 2 f2 = objref.stmid f3 = objref.index else: f1 = 1 f2 = objref[0] # we force all generation numbers to be 0 # f3 = objref[1] f3 = 0 data.append(struct.pack('>B', f1)) data.append(struct.pack('>L', f2)[-fl2:]) data.append(struct.pack('>L', f3)[-fl3:]) index.extend((first, prev - first + 1)) data = zlib.compress(''.join(data)) dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, 'W': [1, fl2, fl3], 'Length': len(data), 'Filter': LITERALS_FLATE_DECODE[0], 'Root': trailer['Root'],} if 'Info' in trailer: dic['Info'] = trailer['Info'] xrefstm = PDFStream(dic, data) self.serialize_indirect(maxobj, xrefstm) self.write('startxref\n%d\n%%%%EOF' % startxref) def write(self, data): self.outf.write(data) self.last = data[-1:] def tell(self): return self.outf.tell() def escape_string(self, string): string = string.replace('\\', '\\\\') string = string.replace('\n', r'\n') string = string.replace('(', r'\(') string = string.replace(')', r'\)') # get rid of ciando id regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}') if regularexp.match(string): return ('http://www.ciando.com') return string def serialize_object(self, obj): if isinstance(obj, dict): # Correct malformed Mac OS resource forks for Stanza if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ and isinstance(obj['Type'], int): obj['Subtype'] = obj['Type'] del obj['Type'] # end - hope this doesn't have bad effects self.write('<<') for key, val in obj.items(): self.write('/%s' % key) self.serialize_object(val) self.write('>>') elif isinstance(obj, list): self.write('[') for val in obj: self.serialize_object(val) self.write(']') elif isinstance(obj, str): self.write('(%s)' % self.escape_string(obj)) elif isinstance(obj, bool): if self.last.isalnum(): self.write(' ') self.write(str(obj).lower()) elif isinstance(obj, (int, long)): if self.last.isalnum(): self.write(' ') self.write(str(obj)) elif isinstance(obj, Decimal): if self.last.isalnum(): self.write(' ') self.write(str(obj)) elif isinstance(obj, PDFObjRef): if self.last.isalnum(): self.write(' ') self.write('%d %d R' % (obj.objid, 0)) elif isinstance(obj, PDFStream): ### If we don't generate cross ref streams the object streams ### are no longer useful, as we have extracted all objects from ### them. Therefore leave them out from the output. if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: self.write('(deleted)') else: data = obj.get_decdata() self.serialize_object(obj.dic) self.write('stream\n') self.write(data) self.write('\nendstream') else: data = str(obj) if data[0].isalnum() and self.last.isalnum(): self.write(' ') self.write(data) def serialize_indirect(self, objid, obj): self.write('%d 0 obj' % (objid,)) self.serialize_object(obj) if self.last.isalnum(): self.write('\n') self.write('endobj\n') def decryptBook(userkey, inpath, outpath): if AES is None: raise IGNOBLEError("PyCrypto or OpenSSL must be installed.") with open(inpath, 'rb') as inf: #try: serializer = PDFSerializer(inf, userkey) #except: # print("Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))) # return 2 # hope this will fix the 'bad file descriptor' problem with open(outpath, 'wb') as outf: # help construct to make sure the method runs to the end try: serializer.dump(outf) except Exception as e: print("error writing pdf: {0}".format(e.args[0])) return 2 return 0 def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) if result == 0: print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) return result def gui_main(): try: import tkinter import tkinter.constants import tkinter.filedialog import tkinter.messagebox import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Select files for decryption") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Key file").grid(row=0) self.keypath = tkinter.Entry(body, width=30) self.keypath.grid(row=0, column=1, sticky=sticky) if os.path.exists("bnpdfkey.b64"): self.keypath.insert(0, "bnpdfkey.b64") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) tkinter.Label(body, text="Input file").grid(row=1) self.inpath = tkinter.Entry(body, width=30) self.inpath.grid(row=1, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_inpath) button.grid(row=1, column=2) tkinter.Label(body, text="Output file").grid(row=2) self.outpath = tkinter.Entry(body, width=30) self.outpath.grid(row=2, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Decrypt", width=10, command=self.decrypt) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.askopenfilename( parent=None, title="Select Barnes & Noble \'.b64\' key file", defaultextension=".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def get_inpath(self): inpath = tkinter.filedialog.askopenfilename( parent=None, title="Select B&N-encrypted PDF file to decrypt", defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, tkinter.constants.END) self.inpath.insert(0, inpath) return def get_outpath(self): outpath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select unencrypted PDF file to produce", defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, tkinter.constants.END) self.outpath.insert(0, outpath) return def decrypt(self): keypath = self.keypath.get() inpath = self.inpath.get() outpath = self.outpath.get() if not keypath or not os.path.exists(keypath): self.status['text'] = "Specified key file does not exist" return if not inpath or not os.path.exists(inpath): self.status['text'] = "Specified input file does not exist" return if not outpath: self.status['text'] = "Output file not specified" return if inpath == outpath: self.status['text'] = "Must have different input and output files" return userkey = open(keypath,'rb').read() self.status['text'] = "Decrypting..." try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception as e: self.status['text'] = "Error; {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = "File successfully decrypted" else: self.status['text'] = "The was an error decrypting the file." root = tkinter.Tk() if AES is None: root.withdraw() tkinter.messagebox.showerror( "IGNOBLE PDF", "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 root.title("Barnes & Noble PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ineptepub.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ineptepub.py # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Revision history: # 1 - Initial release # 2 - Rename to INEPT, fix exit code # 5 - Version bump to avoid (?) confusion; # Improve OS X support by using OpenSSL when available # 5.1 - Improve OpenSSL error checking # 5.2 - Fix ctypes error causing segfaults on some systems # 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o # 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml # 5.5 - On Windows try PyCrypto first, OpenSSL next # 5.6 - Modify interface to allow use with import # 5.7 - Fix for potential problem with PyCrypto # 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code # 5.9 - Fixed to retain zip file metadata (e.g. file modification date) # 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility # 6.1 - Work if TkInter is missing # 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis # 6.3 - Add additional check on DER file sanity # 6.4 - Remove erroneous check on DER file sanity # 6.5 - Completely remove erroneous check on DER file sanity # 6.6 - Import tkFileDialog, don't assume something else will import it. # 7.0 - Add Python 3 compatibility for calibre 5.0 """ Decrypt Adobe Digital Editions encrypted ePub books. """ __license__ = 'GPL v3' __version__ = "7.0" import codecs import sys import os import traceback import zlib import zipfile from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from contextlib import closing import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] return ["ineptepub.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class ADEPTError(Exception): pass def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library if iswindows: libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: raise ADEPTError('libcrypto not found') libcrypto = CDLL(libcrypto) RSA_NO_PADDING = 3 AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class RSA(Structure): pass RSA_p = POINTER(RSA) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', [RSA_p, c_char_pp, c_long]) RSA_size = F(c_int, 'RSA_size', [RSA_p]) RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', [c_int, c_char_p, c_char_p, RSA_p, c_int]) RSA_free = F(None, 'RSA_free', [RSA_p]) AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class RSA(object): def __init__(self, der): buf = create_string_buffer(der) pp = c_char_pp(cast(buf, c_char_p)) rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) if rsa is None: raise ADEPTError('Error parsing ADEPT user key DER') def decrypt(self, from_): rsa = self._rsa to = create_string_buffer(RSA_size(rsa)) dlen = RSA_private_decrypt(len(from_), from_, to, rsa, RSA_NO_PADDING) if dlen < 0: raise ADEPTError('RSA decryption failed') return to[:dlen] def __del__(self): if self._rsa is not None: RSA_free(self._rsa) self._rsa = None class AES(object): def __init__(self, userkey): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise ADEPTError('AES improper key used') return key = self._key = AES_KEY() rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: raise ADEPTError('Failed to initialize AES key') def decrypt(self, data): out = create_string_buffer(len(data)) iv = (b"\x00" * self._blocksize) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: raise ADEPTError('AES decryption failed') return out.raw return (AES, RSA) def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES from Crypto.PublicKey import RSA as _RSA from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 # ASN.1 parsing code from tlslite class ASN1Error(Exception): pass class ASN1Parser(object): class Parser(object): def __init__(self, bytes): self.bytes = bytes self.index = 0 def get(self, length): if self.index + length > len(self.bytes): raise ASN1Error("Error decoding ASN.1") x = 0 for count in range(length): x <<= 8 x |= self.bytes[self.index] self.index += 1 return x def getFixBytes(self, lengthBytes): bytes = self.bytes[self.index : self.index+lengthBytes] self.index += lengthBytes return bytes def getVarBytes(self, lengthLength): lengthBytes = self.get(lengthLength) return self.getFixBytes(lengthBytes) def getFixList(self, length, lengthList): l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def getVarList(self, length, lengthLength): lengthList = self.get(lengthLength) if lengthList % length != 0: raise ASN1Error("Error decoding ASN.1") lengthList = int(lengthList/length) l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def startLengthCheck(self, lengthLength): self.lengthCheck = self.get(lengthLength) self.indexCheck = self.index def setLengthCheck(self, length): self.lengthCheck = length self.indexCheck = self.index def stopLengthCheck(self): if (self.index - self.indexCheck) != self.lengthCheck: raise ASN1Error("Error decoding ASN.1") def atLengthCheck(self): if (self.index - self.indexCheck) < self.lengthCheck: return False elif (self.index - self.indexCheck) == self.lengthCheck: return True else: raise ASN1Error("Error decoding ASN.1") def __init__(self, bytes): p = self.Parser(bytes) p.get(1) self.length = self._getASN1Length(p) self.value = p.getFixBytes(self.length) def getChild(self, which): p = self.Parser(self.value) for x in range(which+1): markIndex = p.index p.get(1) length = self._getASN1Length(p) p.getFixBytes(length) return ASN1Parser(p.bytes[markIndex:p.index]) def _getASN1Length(self, p): firstLength = p.get(1) if firstLength<=127: return firstLength else: lengthLength = firstLength & 0x7F return p.get(lengthLength) class AES(object): def __init__(self, key): self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16) def decrypt(self, data): return self._aes.decrypt(data) class RSA(object): def __init__(self, der): key = ASN1Parser([x for x in der]) key = [key.getChild(x).value for x in range(1, 4)] key = [self.bytesToNumber(v) for v in key] self._rsa = _RSA.construct(key) def bytesToNumber(self, bytes): total = 0 for byte in bytes: total = (total << 8) + byte return total def decrypt(self, data): return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172) return (AES, RSA) def _load_crypto(): AES = RSA = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: AES, RSA = loader() break except (ImportError, ADEPTError): pass return (AES, RSA) AES, RSA = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), enc('CipherReference')) for elem in encryption.findall(expr): path = elem.get('URI', None) if path is not None: path = path.encode('utf-8') encrypted.add(path) def decompress(self, bytes): dc = zlib.decompressobj(-15) try: decompressed_bytes = dc.decompress(bytes) ex = dc.decompress(b'Z') + dc.flush() if ex: decompressed_bytes = decompressed_bytes + ex except: # possibly not compressed by zip - just return bytes return bytes return decompressed_bytes def decrypt(self, path, data): if path.encode('utf-8') in self._encrypted: data = self._aes.decrypt(data)[16:] if type(data[-1]) != int: place = ord(data[-1]) else: place = data[-1] data = data[:-place] data = self.decompress(data) return data # check file to make check whether it's probably an Adobe Adept encrypted ePub def adeptBook(inpath): with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: return False try: rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) == 172: return True except: # if we couldn't check, assume it is return True return False def decryptBook(userkey, inpath, outpath): if AES is None: raise ADEPTError("PyCrypto or OpenSSL must be installed.") rsa = RSA(userkey) with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: print("{0:s} is DRM-free.".format(os.path.basename(inpath))) return 1 for name in META_NAMES: namelist.remove(name) try: rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) != 172: print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))) return 1 bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64')) # Padded as per RSAES-PKCS1-v1_5 if len(bookkey) > 16: if bookkey[-17] == '\x00' or bookkey[-17] == 0: bookkey = bookkey[-16:] else: print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) return 2 encryption = inf.read('META-INF/encryption.xml') decryptor = Decryptor(bookkey, encryption) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: zi = ZipInfo('mimetype') zi.compress_type=ZIP_STORED try: # if the mimetype is present, get its info, including time-stamp oldzi = inf.getinfo('mimetype') # copy across fields to be preserved zi.date_time = oldzi.date_time zi.comment = oldzi.comment zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr zi.create_system = oldzi.create_system except: pass outf.writestr(zi, inf.read('mimetype')) for path in namelist: data = inf.read(path) zi = ZipInfo(path) zi.compress_type=ZIP_DEFLATED try: # get the file info, including time-stamp oldzi = inf.getinfo(path) # copy across useful fields zi.date_time = oldzi.date_time zi.comment = oldzi.comment zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr zi.create_system = oldzi.create_system except: pass outf.writestr(zi, decryptor.decrypt(path, data)) except: print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) return 2 return 0 def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) if result == 0: print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) return result def gui_main(): try: import tkinter import tkinter.constants import tkinter.filedialog import tkinter.messagebox import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Select files for decryption") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Key file").grid(row=0) self.keypath = tkinter.Entry(body, width=30) self.keypath.grid(row=0, column=1, sticky=sticky) if os.path.exists("adeptkey.der"): self.keypath.insert(0, "adeptkey.der") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) tkinter.Label(body, text="Input file").grid(row=1) self.inpath = tkinter.Entry(body, width=30) self.inpath.grid(row=1, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_inpath) button.grid(row=1, column=2) tkinter.Label(body, text="Output file").grid(row=2) self.outpath = tkinter.Entry(body, width=30) self.outpath.grid(row=2, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Decrypt", width=10, command=self.decrypt) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.askopenfilename( parent=None, title="Select Adobe Adept \'.der\' key file", defaultextension=".der", filetypes=[('Adobe Adept DER-encoded files', '.der'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def get_inpath(self): inpath = tkinter.filedialog.askopenfilename( parent=None, title="Select ADEPT-encrypted ePub file to decrypt", defaultextension=".epub", filetypes=[('ePub files', '.epub')]) if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, tkinter.constants.END) self.inpath.insert(0, inpath) return def get_outpath(self): outpath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select unencrypted ePub file to produce", defaultextension=".epub", filetypes=[('ePub files', '.epub')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, tkinter.constants.END) self.outpath.insert(0, outpath) return def decrypt(self): keypath = self.keypath.get() inpath = self.inpath.get() outpath = self.outpath.get() if not keypath or not os.path.exists(keypath): self.status['text'] = "Specified key file does not exist" return if not inpath or not os.path.exists(inpath): self.status['text'] = "Specified input file does not exist" return if not outpath: self.status['text'] = "Output file not specified" return if inpath == outpath: self.status['text'] = "Must have different input and output files" return userkey = open(keypath,'rb').read() self.status['text'] = "Decrypting..." try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception as e: self.status['text'] = "Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = "File successfully decrypted" else: self.status['text'] = "There was an error decrypting the file." root = tkinter.Tk() root.title("Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ineptpdf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ineptpdf.py # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. # Released under the terms of the GNU General Public Licence, version 3 # # Revision history: # 1 - Initial release # 2 - Improved determination of key-generation algorithm # 3 - Correctly handle PDF >=1.5 cross-reference streams # 4 - Removal of ciando's personal ID # 5 - Automated decryption of a complete directory # 6.1 - backward compatibility for 1.7.1 and old adeptkey.der # 7 - Get cross reference streams and object streams working for input. # Not yet supported on output but this only effects file size, # not functionality. (anon2) # 7.1 - Correct a problem when an old trailer is not followed by startxref # 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) # - Support for cross ref streams on output (decreases file size) # 7.3 - Correct bug in trailer with cross ref stream that caused the error # "The root object is missing or invalid" in Adobe Reader. (anon2) # 7.4 - Force all generation numbers in output file to be 0, like in v6. # Fallback code for wrong xref improved (search till last trailer # instead of first) (anon2) # 7.5 - allow support for OpenSSL to replace pycrypto on all platforms # implemented ARC4 interface to OpenSSL # fixed minor typos # 7.6 - backported AES and other fixes from version 8.4.48 # 7.7 - On Windows try PyCrypto first and OpenSSL next # 7.8 - Modify interface to allow use of import # 7.9 - Bug fix for some session key errors when len(bookkey) > length required # 7.10 - Various tweaks to fix minor problems. # 7.11 - More tweaks to fix minor problems. # 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code # 7.13 - Fixed erroneous mentions of ineptepub # 7.14 - moved unicode_argv call inside main for Windows DeDRM compatibility # 8.0 - Work if TkInter is missing # 8.0.1 - Broken Metadata fix. # 8.0.2 - Add additional check on DER file sanity # 8.0.3 - Remove erroneous check on DER file sanity # 8.0.4 - Completely remove erroneous check on DER file sanity # 8.0.5 - Do not process DRM-free documents # 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 """ Decrypts Adobe ADEPT-encrypted PDF files. """ __license__ = 'GPL v3' __version__ = "9.0.0" import codecs import sys import os import re import zlib import struct import hashlib from io import BytesIO from decimal import Decimal import itertools import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] return ["ineptpdf.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class ADEPTError(Exception): pass import hashlib def SHA256(message): ctx = hashlib.sha256() ctx.update(message) return ctx.digest() def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library if sys.platform.startswith('win'): libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: raise ADEPTError('libcrypto not found') libcrypto = CDLL(libcrypto) AES_MAXNR = 14 RSA_NO_PADDING = 3 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) class RC4_KEY(Structure): _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] RC4_KEY_p = POINTER(RC4_KEY) class RSA(Structure): pass RSA_p = POINTER(RSA) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func 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]) 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]) d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', [RSA_p, c_char_pp, c_long]) RSA_size = F(c_int, 'RSA_size', [RSA_p]) RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', [c_int, c_char_p, c_char_p, RSA_p, c_int]) RSA_free = F(None, 'RSA_free', [RSA_p]) class RSA(object): def __init__(self, der): buf = create_string_buffer(der) pp = c_char_pp(cast(buf, c_char_p)) rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) if rsa is None: raise ADEPTError('Error parsing ADEPT user key DER') def decrypt(self, from_): rsa = self._rsa to = create_string_buffer(RSA_size(rsa)) dlen = RSA_private_decrypt(len(from_), from_, to, rsa, RSA_NO_PADDING) if dlen < 0: raise ADEPTError('RSA decryption failed') return to[1:dlen] def __del__(self): if self._rsa is not None: RSA_free(self._rsa) self._rsa = None class ARC4(object): @classmethod def new(cls, userkey): self = ARC4() self._blocksize = len(userkey) key = self._key = RC4_KEY() RC4_set_key(key, self._blocksize, userkey) return self def __init__(self): self._blocksize = 0 self._key = None def decrypt(self, data): out = create_string_buffer(len(data)) RC4_crypt(self._key, len(data), data, out) return out.raw class AES(object): MODE_CBC = 0 @classmethod def new(cls, userkey, mode, iv): self = AES() self._blocksize = len(userkey) # mode is ignored since CBCMODE is only thing supported/used so far self._mode = mode if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise ADEPTError('AES improper key used') return keyctx = self._keyctx = AES_KEY() self._iv = iv rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: raise ADEPTError('Failed to initialize AES key') return self def __init__(self): self._blocksize = 0 self._keyctx = None self._iv = 0 self._mode = 0 def decrypt(self, data): out = create_string_buffer(len(data)) rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) if rv == 0: raise ADEPTError('AES decryption failed') return out.raw return (ARC4, RSA, AES) def _load_crypto_pycrypto(): from Crypto.PublicKey import RSA as _RSA from Crypto.Cipher import ARC4 as _ARC4 from Crypto.Cipher import AES as _AES from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 # ASN.1 parsing code from tlslite class ASN1Error(Exception): pass class ASN1Parser(object): class Parser(object): def __init__(self, bytes): self.bytes = bytes self.index = 0 def get(self, length): if self.index + length > len(self.bytes): raise ASN1Error("Error decoding ASN.1") x = 0 for count in range(length): x <<= 8 x |= self.bytes[self.index] self.index += 1 return x def getFixBytes(self, lengthBytes): bytes = self.bytes[self.index : self.index+lengthBytes] self.index += lengthBytes return bytes def getVarBytes(self, lengthLength): lengthBytes = self.get(lengthLength) return self.getFixBytes(lengthBytes) def getFixList(self, length, lengthList): l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def getVarList(self, length, lengthLength): lengthList = self.get(lengthLength) if lengthList % length != 0: raise ASN1Error("Error decoding ASN.1") lengthList = int(lengthList/length) l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def startLengthCheck(self, lengthLength): self.lengthCheck = self.get(lengthLength) self.indexCheck = self.index def setLengthCheck(self, length): self.lengthCheck = length self.indexCheck = self.index def stopLengthCheck(self): if (self.index - self.indexCheck) != self.lengthCheck: raise ASN1Error("Error decoding ASN.1") def atLengthCheck(self): if (self.index - self.indexCheck) < self.lengthCheck: return False elif (self.index - self.indexCheck) == self.lengthCheck: return True else: raise ASN1Error("Error decoding ASN.1") def __init__(self, bytes): p = self.Parser(bytes) p.get(1) self.length = self._getASN1Length(p) self.value = p.getFixBytes(self.length) def getChild(self, which): p = self.Parser(self.value) for x in range(which+1): markIndex = p.index p.get(1) length = self._getASN1Length(p) p.getFixBytes(length) return ASN1Parser(p.bytes[markIndex:p.index]) def _getASN1Length(self, p): firstLength = p.get(1) if firstLength<=127: return firstLength else: lengthLength = firstLength & 0x7F return p.get(lengthLength) class ARC4(object): @classmethod def new(cls, userkey): self = ARC4() self._arc4 = _ARC4.new(userkey) return self def __init__(self): self._arc4 = None def decrypt(self, data): return self._arc4.decrypt(data) class AES(object): MODE_CBC = _AES.MODE_CBC @classmethod def new(cls, userkey, mode, iv): self = AES() self._aes = _AES.new(userkey, mode, iv) return self def __init__(self): self._aes = None def decrypt(self, data): return self._aes.decrypt(data) class RSA(object): def __init__(self, der): key = ASN1Parser([x for x in der]) key = [key.getChild(x).value for x in range(1, 4)] key = [self.bytesToNumber(v) for v in key] self._rsa = _RSA.construct(key) def bytesToNumber(self, bytes): total = 0 for byte in bytes: total = (total << 8) + byte return total def decrypt(self, data): return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172) return (ARC4, RSA, AES) def _load_crypto(): ARC4 = RSA = AES = None cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) if sys.platform.startswith('win'): cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) for loader in cryptolist: try: ARC4, RSA, AES = loader() break except (ImportError, ADEPTError): pass return (ARC4, RSA, AES) ARC4, RSA, AES = _load_crypto() # Do we generate cross reference streams on output? # 0 = never # 1 = only if present in input # 2 = always GEN_XREF_STM = 1 # This is the value for the current document gen_xref_stm = False # will be set in PDFSerializer # PDF parsing routines from pdfminer, with changes for EBX_HANDLER # Utilities def choplist(n, seq): '''Groups every n elements of the list.''' r = [] for x in seq: r.append(x) if len(r) == n: yield tuple(r) r = [] return def nunpack(s, default=0): '''Unpacks up to 4 bytes big endian.''' l = len(s) if not l: return default elif l == 1: return ord(s) elif l == 2: return struct.unpack('>H', s)[0] elif l == 3: return struct.unpack('>L', b'\x00'+s)[0] elif l == 4: return struct.unpack('>L', s)[0] else: return TypeError('invalid length: %d' % l) STRICT = 0 # PS Exceptions class PSException(Exception): pass class PSEOF(PSException): pass class PSSyntaxError(PSException): pass class PSTypeError(PSException): pass class PSValueError(PSException): pass # Basic PostScript Types # PSLiteral class PSObject(object): pass class PSLiteral(PSObject): ''' PS literals (e.g. "/Name"). Caution: Never create these objects directly. Use PSLiteralTable.intern() instead. ''' def __init__(self, name): self.name = name.decode('utf-8') return def __repr__(self): name = [] for char in self.name: if not char.isalnum(): char = '#%02x' % ord(char) name.append(char) return '/%s' % ''.join(name) # PSKeyword class PSKeyword(PSObject): ''' PS keywords (e.g. "showpage"). Caution: Never create these objects directly. Use PSKeywordTable.intern() instead. ''' def __init__(self, name): self.name = name.decode('utf-8') return def __repr__(self): return self.name # PSSymbolTable class PSSymbolTable(object): ''' Symbol table that stores PSLiteral or PSKeyword. ''' def __init__(self, classe): self.dic = {} self.classe = classe return def intern(self, name): if name in self.dic: lit = self.dic[name] else: lit = self.classe(name) self.dic[name] = lit return lit PSLiteralTable = PSSymbolTable(PSLiteral) PSKeywordTable = PSSymbolTable(PSKeyword) LIT = PSLiteralTable.intern KWD = PSKeywordTable.intern KEYWORD_BRACE_BEGIN = KWD(b'{') KEYWORD_BRACE_END = KWD(b'}') KEYWORD_ARRAY_BEGIN = KWD(b'[') KEYWORD_ARRAY_END = KWD(b']') KEYWORD_DICT_BEGIN = KWD(b'<<') KEYWORD_DICT_END = KWD(b'>>') def literal_name(x): if not isinstance(x, PSLiteral): if STRICT: raise PSTypeError('Literal required: %r' % x) else: return str(x) return x.name def keyword_name(x): if not isinstance(x, PSKeyword): if STRICT: raise PSTypeError('Keyword required: %r' % x) else: return str(x) return x.name ## PSBaseParser ## EOL = re.compile(rb'[\r\n]') SPC = re.compile(rb'\s') NONSPC = re.compile(rb'\S') HEX = re.compile(rb'[0-9a-fA-F]') END_LITERAL = re.compile(rb'[#/%\[\]()<>{}\s]') END_HEX_STRING = re.compile(rb'[^\s0-9a-fA-F]') HEX_PAIR = re.compile(rb'[0-9a-fA-F]{2}|.') END_NUMBER = re.compile(rb'[^0-9]') END_KEYWORD = re.compile(rb'[#/%\[\]()<>{}\s]') END_STRING = re.compile(rb'[()\\]') OCT_STRING = re.compile(rb'[0-7]') ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 } class PSBaseParser(object): ''' Most basic PostScript parser that performs only basic tokenization. ''' BUFSIZ = 4096 def __init__(self, fp): self.fp = fp self.seek(0) return def __repr__(self): return '' % (self.fp, self.bufpos) def flush(self): return def close(self): self.flush() return def tell(self): return self.bufpos+self.charpos def poll(self, pos=None, n=80): pos0 = self.fp.tell() if not pos: pos = self.bufpos+self.charpos self.fp.seek(pos) self.fp.seek(pos0) return def seek(self, pos): ''' Seeks the parser to the given position. ''' self.fp.seek(pos) # reset the status for nextline() self.bufpos = pos self.buf = b'' self.charpos = 0 # reset the status for nexttoken() self.parse1 = self.parse_main self.tokens = [] return def fillbuf(self): if self.charpos < len(self.buf): return # fetch next chunk. self.bufpos = self.fp.tell() self.buf = self.fp.read(self.BUFSIZ) if not self.buf: raise PSEOF('Unexpected EOF') self.charpos = 0 return def parse_main(self, s, i): m = NONSPC.search(s, i) if not m: return (self.parse_main, len(s)) j = m.start(0) c = bytes([s[j]]) self.tokenstart = self.bufpos+j if c == b'%': self.token = c return (self.parse_comment, j+1) if c == b'/': self.token = b'' return (self.parse_literal, j+1) if c in b'-+' or c.isdigit(): self.token = c return (self.parse_number, j+1) if c == b'.': self.token = c return (self.parse_decimal, j+1) if c.isalpha(): self.token = c return (self.parse_keyword, j+1) if c == b'(': self.token = b'' self.paren = 1 return (self.parse_string, j+1) if c == b'<': self.token = b'' return (self.parse_wopen, j+1) if c == b'>': self.token = b'' return (self.parse_wclose, j+1) self.add_token(KWD(c)) return (self.parse_main, j+1) def add_token(self, obj): self.tokens.append((self.tokenstart, obj)) return def parse_comment(self, s, i): m = EOL.search(s, i) if not m: self.token += s[i:] return (self.parse_comment, len(s)) j = m.start(0) self.token += s[i:j] # We ignore comments. #self.tokens.append(self.token) return (self.parse_main, j) def parse_literal(self, s, i): m = END_LITERAL.search(s, i) if not m: self.token += s[i:] return (self.parse_literal, len(s)) j = m.start(0) self.token += s[i:j] c = bytes([s[j]]) if c == b'#': self.hex = b'' return (self.parse_literal_hex, j+1) self.add_token(LIT(self.token)) return (self.parse_main, j) def parse_literal_hex(self, s, i): c = bytes([s[i]]) if HEX.match(c) and len(self.hex) < 2: self.hex += c return (self.parse_literal_hex, i+1) if self.hex: self.token += bytes([int(self.hex, 16)]) return (self.parse_literal, i) def parse_number(self, s, i): m = END_NUMBER.search(s, i) if not m: self.token += s[i:] return (self.parse_number, len(s)) j = m.start(0) self.token += s[i:j] c = bytes([s[j]]) if c == b'.': self.token += c return (self.parse_decimal, j+1) try: self.add_token(int(self.token)) except ValueError: pass return (self.parse_main, j) def parse_decimal(self, s, i): m = END_NUMBER.search(s, i) if not m: self.token += s[i:] return (self.parse_decimal, len(s)) j = m.start(0) self.token += s[i:j] self.add_token(Decimal(self.token.decode('utf-8'))) return (self.parse_main, j) def parse_keyword(self, s, i): m = END_KEYWORD.search(s, i) if not m: self.token += s[i:] return (self.parse_keyword, len(s)) j = m.start(0) self.token += s[i:j] if self.token == 'true': token = True elif self.token == 'false': token = False else: token = KWD(self.token) self.add_token(token) return (self.parse_main, j) def parse_string(self, s, i): m = END_STRING.search(s, i) if not m: self.token += s[i:] return (self.parse_string, len(s)) j = m.start(0) self.token += s[i:j] c = bytes([s[j]]) if c == b'\\': self.oct = '' return (self.parse_string_1, j+1) if c == b'(': self.paren += 1 self.token += c return (self.parse_string, j+1) if c == b')': self.paren -= 1 if self.paren: self.token += c return (self.parse_string, j+1) self.add_token(self.token) return (self.parse_main, j+1) def parse_string_1(self, s, i): c = bytes([s[i]]) if OCT_STRING.match(c) and len(self.oct) < 3: self.oct += c return (self.parse_string_1, i+1) if self.oct: self.token += bytes([int(self.oct, 8)]) return (self.parse_string, i) if c in ESC_STRING: self.token += bytes([ESC_STRING[c]]) return (self.parse_string, i+1) def parse_wopen(self, s, i): c = bytes([s[i]]) if c.isspace() or HEX.match(c): return (self.parse_hexstring, i) if c == b'<': self.add_token(KEYWORD_DICT_BEGIN) i += 1 return (self.parse_main, i) def parse_wclose(self, s, i): c = bytes([s[i]]) if c == b'>': self.add_token(KEYWORD_DICT_END) i += 1 return (self.parse_main, i) def parse_hexstring(self, s, i): m1 = END_HEX_STRING.search(s, i) if not m1: self.token += s[i:] return (self.parse_hexstring, len(s)) j = m1.start(0) self.token += s[i:j] token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]), SPC.sub(b'', self.token)) self.add_token(token) return (self.parse_main, j) def nexttoken(self): while not self.tokens: self.fillbuf() (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) token = self.tokens.pop(0) return token def nextline(self): ''' Fetches a next line that ends either with \\r or \\n. ''' linebuf = b'' linepos = self.bufpos + self.charpos eol = False while 1: self.fillbuf() if eol: c = bytes([self.buf[self.charpos]]) # handle '\r\n' if c == b'\n': linebuf += c self.charpos += 1 break m = EOL.search(self.buf, self.charpos) if m: linebuf += self.buf[self.charpos:m.end(0)] self.charpos = m.end(0) if bytes([linebuf[-1]]) == b'\r': eol = True else: break else: linebuf += self.buf[self.charpos:] self.charpos = len(self.buf) return (linepos, linebuf) def revreadlines(self): ''' Fetches a next line backword. This is used to locate the trailers at the end of a file. ''' self.fp.seek(0, 2) pos = self.fp.tell() buf = b'' while 0 < pos: prevpos = pos pos = max(0, pos-self.BUFSIZ) self.fp.seek(pos) s = self.fp.read(prevpos-pos) if not s: break while 1: n = max(s.rfind(b'\r'), s.rfind(b'\n')) if n == -1: buf = s + buf break yield s[n:]+buf s = s[:n] buf = b'' return ## PSStackParser ## class PSStackParser(PSBaseParser): def __init__(self, fp): PSBaseParser.__init__(self, fp) self.reset() return def reset(self): self.context = [] self.curtype = None self.curstack = [] self.results = [] return def seek(self, pos): PSBaseParser.seek(self, pos) self.reset() return def push(self, *objs): self.curstack.extend(objs) return def pop(self, n): objs = self.curstack[-n:] self.curstack[-n:] = [] return objs def popall(self): objs = self.curstack self.curstack = [] return objs def add_results(self, *objs): self.results.extend(objs) return def start_type(self, pos, type): self.context.append((pos, self.curtype, self.curstack)) (self.curtype, self.curstack) = (type, []) return def end_type(self, type): if self.curtype != type: raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) objs = [ obj for (_,obj) in self.curstack ] (pos, self.curtype, self.curstack) = self.context.pop() return (pos, objs) def do_keyword(self, pos, token): return def nextobject(self, direct=False): ''' Yields a list of objects: keywords, literals, strings (byte arrays), numbers, arrays and dictionaries. Arrays and dictionaries are represented as Python sequence and dictionaries. ''' while not self.results: (pos, token) = self.nexttoken() if (isinstance(token, int) or isinstance(token, Decimal) or isinstance(token, bool) or isinstance(token, bytearray) or isinstance(token, bytes) or isinstance(token, PSLiteral)): # normal token self.push((pos, token)) elif token == KEYWORD_ARRAY_BEGIN: # begin array self.start_type(pos, 'a') elif token == KEYWORD_ARRAY_END: # end array try: self.push(self.end_type('a')) except PSTypeError: if STRICT: raise elif token == KEYWORD_DICT_BEGIN: # begin dictionary self.start_type(pos, 'd') elif token == KEYWORD_DICT_END: # end dictionary try: (pos, objs) = self.end_type('d') if len(objs) % 2 != 0: print("Incomplete dictionary construct") objs.append("") # this isn't necessary. # temporary fix. is this due to rental books? # raise PSSyntaxError( # 'Invalid dictionary construct: %r' % objs) d = dict((literal_name(k), v) \ for (k,v) in choplist(2, objs)) self.push((pos, d)) except PSTypeError: if STRICT: raise else: self.do_keyword(pos, token) if self.context: continue else: if direct: return self.pop(1)[0] self.flush() obj = self.results.pop(0) return obj LITERAL_CRYPT = LIT(b'Crypt') LITERALS_FLATE_DECODE = (LIT(b'FlateDecode'), LIT(b'Fl')) LITERALS_LZW_DECODE = (LIT(b'LZWDecode'), LIT(b'LZW')) LITERALS_ASCII85_DECODE = (LIT(b'ASCII85Decode'), LIT(b'A85')) ## PDF Objects ## class PDFObject(PSObject): pass class PDFException(PSException): pass class PDFTypeError(PDFException): pass class PDFValueError(PDFException): pass class PDFNotImplementedError(PSException): pass ## PDFObjRef ## class PDFObjRef(PDFObject): def __init__(self, doc, objid, genno): if objid == 0: if STRICT: raise PDFValueError('PDF object id cannot be 0.') self.doc = doc self.objid = objid self.genno = genno return def __repr__(self): return '' % (self.objid, self.genno) def resolve(self): return self.doc.getobj(self.objid) # resolve def resolve1(x): ''' Resolve an object. If this is an array or dictionary, it may still contains some indirect objects inside. ''' while isinstance(x, PDFObjRef): x = x.resolve() return x def resolve_all(x): ''' Recursively resolve X and all the internals. Make sure there is no indirect reference within the nested object. This procedure might be slow. ''' while isinstance(x, PDFObjRef): x = x.resolve() if isinstance(x, list): x = [ resolve_all(v) for v in x ] elif isinstance(x, dict): for (k,v) in iter(x.items()): x[k] = resolve_all(v) return x def decipher_all(decipher, objid, genno, x): ''' Recursively decipher X. ''' if isinstance(x, bytearray) or isinstance(x,bytes): return decipher(objid, genno, x) decf = lambda v: decipher_all(decipher, objid, genno, v) if isinstance(x, list): x = [decf(v) for v in x] elif isinstance(x, dict): x = dict((k, decf(v)) for (k, v) in iter(x.items())) return x # Type cheking def int_value(x): x = resolve1(x) if not isinstance(x, int): if STRICT: raise PDFTypeError('Integer required: %r' % x) return 0 return x def decimal_value(x): x = resolve1(x) if not isinstance(x, Decimal): if STRICT: raise PDFTypeError('Decimal required: %r' % x) return 0.0 return x def num_value(x): x = resolve1(x) if not (isinstance(x, int) or isinstance(x, Decimal)): if STRICT: raise PDFTypeError('Int or Float required: %r' % x) return 0 return x def str_value(x): x = resolve1(x) if not (isinstance(x, bytearray) or isinstance(x, bytes)): if STRICT: raise PDFTypeError('String required: %r' % x) return '' return x def list_value(x): x = resolve1(x) if not (isinstance(x, list) or isinstance(x, tuple)): if STRICT: raise PDFTypeError('List required: %r' % x) return [] return x def dict_value(x): x = resolve1(x) if not isinstance(x, dict): if STRICT: raise PDFTypeError('Dict required: %r' % x) return {} return x def stream_value(x): x = resolve1(x) if not isinstance(x, PDFStream): if STRICT: raise PDFTypeError('PDFStream required: %r' % x) return PDFStream({}, '') return x # ascii85decode(data) def ascii85decode(data): n = b = 0 out = b'' for c in data: if b'!' <= c and c <= b'u': n += 1 b = b*85+(c-33) if n == 5: out += struct.pack('>L',b) n = b = 0 elif c == b'z': assert n == 0 out += b'\0\0\0\0' elif c == b'~': if n: for _ in range(5-n): b = b*85+84 out += struct.pack('>L',b)[:n-1] break return out ## PDFStream type class PDFStream(PDFObject): def __init__(self, dic, rawdata, decipher=None): length = int_value(dic.get('Length', 0)) eol = rawdata[length:] # quick and dirty fix for false length attribute, # might not work if the pdf stream parser has a problem if decipher != None and decipher.__name__ == 'decrypt_aes': if (len(rawdata) % 16) != 0: cutdiv = len(rawdata) // 16 rawdata = rawdata[:16*cutdiv] else: if eol in (b'\r', b'\n', b'\r\n'): rawdata = rawdata[:length] self.dic = dic self.rawdata = rawdata self.decipher = decipher self.data = None self.decdata = None self.objid = None self.genno = None return def set_objid(self, objid, genno): self.objid = objid self.genno = genno return def __repr__(self): if self.rawdata: return '' % \ (self.objid, len(self.rawdata), self.dic) else: return '' % \ (self.objid, len(self.data), self.dic) def decode(self): assert self.data is None and self.rawdata is not None data = self.rawdata if self.decipher: # Handle encryption data = self.decipher(self.objid, self.genno, data) if gen_xref_stm: self.decdata = data # keep decrypted data if 'Filter' not in self.dic: self.data = data self.rawdata = None return filters = self.dic['Filter'] if not isinstance(filters, list): filters = [ filters ] for f in filters: if f in LITERALS_FLATE_DECODE: # will get errors if the document is encrypted. data = zlib.decompress(data) elif f in LITERALS_LZW_DECODE: data = b''.join(LZWDecoder(BytesIO(data)).run()) elif f in LITERALS_ASCII85_DECODE: data = ascii85decode(data) elif f == LITERAL_CRYPT: raise PDFNotImplementedError('/Crypt filter is unsupported') else: raise PDFNotImplementedError('Unsupported filter: %r' % f) # apply predictors if 'DP' in self.dic: params = self.dic['DP'] else: params = self.dic.get('DecodeParms', {}) if 'Predictor' in params: pred = int_value(params['Predictor']) if pred: if pred != 12: raise PDFNotImplementedError( 'Unsupported predictor: %r' % pred) if 'Columns' not in params: raise PDFValueError( 'Columns undefined for predictor=12') columns = int_value(params['Columns']) buf = b'' ent0 = b'\x00' * columns for i in range(0, len(data), columns+1): pred = data[i] ent1 = data[i+1:i+1+columns] if pred == b'\x02': ent1 = b''.join(bytes([(a+b) & 255]) \ for (a,b) in zip(ent0,ent1)) buf += ent1 ent0 = ent1 data = buf self.data = data self.rawdata = None return def get_data(self): if self.data is None: self.decode() return self.data def get_rawdata(self): return self.rawdata def get_decdata(self): if self.decdata is not None: return self.decdata data = self.rawdata if self.decipher and data: # Handle encryption data = self.decipher(self.objid, self.genno, data) return data ## PDF Exceptions ## class PDFSyntaxError(PDFException): pass class PDFNoValidXRef(PDFSyntaxError): pass class PDFEncryptionError(PDFException): pass class PDFPasswordIncorrect(PDFEncryptionError): pass # some predefined literals and keywords. LITERAL_OBJSTM = LIT(b'ObjStm') LITERAL_XREF = LIT(b'XRef') LITERAL_PAGE = LIT(b'Page') LITERAL_PAGES = LIT(b'Pages') LITERAL_CATALOG = LIT(b'Catalog') ## XRefs ## ## PDFXRef ## class PDFXRef(object): def __init__(self): self.offsets = None return def __repr__(self): return '' % len(self.offsets) def objids(self): return iter(self.offsets.keys()) def load(self, parser): self.offsets = {} while 1: try: (pos, line) = parser.nextline() except PSEOF: raise PDFNoValidXRef('Unexpected EOF - file corrupted?') if not line: raise PDFNoValidXRef('Premature eof: %r' % parser) if line.startswith(b'trailer'): parser.seek(pos) break f = line.strip().split(b' ') if len(f) != 2: raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) try: (start, nobjs) = map(int, f) except ValueError: raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) for objid in range(start, start+nobjs): try: (_, line) = parser.nextline() except PSEOF: raise PDFNoValidXRef('Unexpected EOF - file corrupted?') f = line.strip().split(b' ') if len(f) != 3: raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) (pos, genno, use) = f if use != b'n': continue self.offsets[objid] = (int(genno.decode('utf-8')), int(pos.decode('utf-8'))) self.load_trailer(parser) return KEYWORD_TRAILER = KWD(b'trailer') def load_trailer(self, parser): try: (_,kwd) = parser.nexttoken() assert kwd is self.KEYWORD_TRAILER (_,dic) = parser.nextobject(direct=True) except PSEOF: x = parser.pop(1) if not x: raise PDFNoValidXRef('Unexpected EOF - file corrupted') (_,dic) = x[0] self.trailer = dict_value(dic) return def getpos(self, objid): try: (genno, pos) = self.offsets[objid] except KeyError: raise return (None, pos) ## PDFXRefStream ## class PDFXRefStream(object): def __init__(self): self.index = None self.data = None self.entlen = None self.fl1 = self.fl2 = self.fl3 = None return def __repr__(self): return '' % self.index def objids(self): for first, size in self.index: for objid in range(first, first + size): yield objid def load(self, parser, debug=0): (_,objid) = parser.nexttoken() # ignored (_,genno) = parser.nexttoken() # ignored (_,kwd) = parser.nexttoken() (_,stream) = parser.nextobject() if not isinstance(stream, PDFStream) or \ stream.dic['Type'] is not LITERAL_XREF: raise PDFNoValidXRef('Invalid PDF stream spec.') size = stream.dic['Size'] index = stream.dic.get('Index', (0,size)) self.index = zip(itertools.islice(index, 0, None, 2), itertools.islice(index, 1, None, 2)) (self.fl1, self.fl2, self.fl3) = stream.dic['W'] self.data = stream.get_data() self.entlen = self.fl1+self.fl2+self.fl3 self.trailer = stream.dic return def getpos(self, objid): offset = 0 for first, size in self.index: if first <= objid and objid < (first + size): break offset += size else: raise KeyError(objid) i = self.entlen * ((objid - first) + offset) ent = self.data[i:i+self.entlen] f1 = nunpack(ent[:self.fl1], 1) if f1 == 1: pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) genno = nunpack(ent[self.fl1+self.fl2:]) return (None, pos) elif f1 == 2: objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) index = nunpack(ent[self.fl1+self.fl2:]) return (objid, index) # this is a free object raise KeyError(objid) ## PDFDocument ## ## A PDFDocument object represents a PDF document. ## Since a PDF file is usually pretty big, normally it is not loaded ## at once. Rather it is parsed dynamically as processing goes. ## A PDF parser is associated with the document. ## class PDFDocument(object): def __init__(self): self.xrefs = [] self.objs = {} self.parsed_objs = {} self.root = None self.catalog = None self.parser = None self.encryption = None self.decipher = None return # set_parser(parser) # Associates the document with an (already initialized) parser object. def set_parser(self, parser): if self.parser: return self.parser = parser # The document is set to be temporarily ready during collecting # all the basic information about the document, e.g. # the header, the encryption information, and the access rights # for the document. self.ready = True # Retrieve the information of each header that was appended # (maybe multiple times) at the end of the document. self.xrefs = parser.read_xref() for xref in self.xrefs: trailer = xref.trailer if not trailer: continue # If there's an encryption info, remember it. if 'Encrypt' in trailer: #assert not self.encryption try: self.encryption = (list_value(trailer['ID']), dict_value(trailer['Encrypt'])) # fix for bad files except: self.encryption = (b'ffffffffffffffffffffffffffffffffffff', dict_value(trailer['Encrypt'])) if 'Root' in trailer: self.set_root(dict_value(trailer['Root'])) break else: raise PDFSyntaxError('No /Root object! - Is this really a PDF?') # The document is set to be non-ready again, until all the # proper initialization (asking the password key and # verifying the access permission, so on) is finished. self.ready = False return # set_root(root) # Set the Root dictionary of the document. # Each PDF file must have exactly one /Root dictionary. def set_root(self, root): self.root = root self.catalog = dict_value(self.root) if self.catalog.get('Type') is not LITERAL_CATALOG: if STRICT: raise PDFSyntaxError('Catalog not found!') return # initialize(password='') # Perform the initialization with a given password. # This step is mandatory even if there's no password associated # with the document. def initialize(self, password=b''): if not self.encryption: self.is_printable = self.is_modifiable = self.is_extractable = True self.ready = True raise PDFEncryptionError('Document is not encrypted.') return (docid, param) = self.encryption type = literal_name(param['Filter']) if type == 'Adobe.APS': return self.initialize_adobe_ps(password, docid, param) if type == 'Standard': return self.initialize_standard(password, docid, param) if type == 'EBX_HANDLER': return self.initialize_ebx(password, docid, param) raise PDFEncryptionError('Unknown filter: param=%r' % param) def initialize_adobe_ps(self, password, docid, param): global KEYFILEPATH self.decrypt_key = self.genkey_adobe_ps(param) self.genkey = self.genkey_v4 self.decipher = self.decrypt_aes self.ready = True return def genkey_adobe_ps(self, param): # nice little offline principal keys dictionary # global static principal key for German Onleihe / Bibliothek Digital principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')} self.is_printable = self.is_modifiable = self.is_extractable = True length = int_value(param.get('Length', 0)) // 8 edcdata = str_value(param.get('EDCData')).decode('base64') pdrllic = str_value(param.get('PDRLLic')).decode('base64') pdrlpol = str_value(param.get('PDRLPol')).decode('base64') edclist = [] for pair in edcdata.split(b'\n'): edclist.append(pair) # principal key request for key in principalkeys: if key in pdrllic: principalkey = principalkeys[key] else: raise ADEPTError('Cannot find principal key for this pdf') shakey = SHA256(principalkey) ivector = bytes(16) # 16 zero bytes plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) if plaintext[-16:] != bytearray(b'\0x10')*16: raise ADEPTError('Offlinekey cannot be decrypted, aborting ...') pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) if pdrlpol[-1] < 1 or pdrlpol[-1] > 16: raise ADEPTError('Could not decrypt PDRLPol, aborting ...') else: cutter = -1 * pdrlpol[-1] pdrlpol = pdrlpol[:cutter] return plaintext[:16] PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' # experimental aes pw support def initialize_standard(self, password, docid, param): # copy from a global variable 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) O = str_value(param['O']) R = int_value(param['R']) # Revision if 5 <= R: raise PDFEncryptionError('Unknown revision: %r' % R) U = str_value(param['U']) 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 password = (password+self.PASSWORD_PADDING)[:32] # 1 hash = hashlib.md5(password) # 2 hash.update(O) # 3 hash.update(struct.pack('= 3: # Algorithm 3.5 hash = hashlib.md5(self.PASSWORD_PADDING) # 2 hash.update(docid[0]) # 3 x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 for i in range(1,19+1): k = b''.join(bytes([c ^ i]) for c in key ) x = ARC4.new(k).decrypt(x) u1 = x+x # 32bytes total if R == 2: is_authenticated = (u1 == U) else: is_authenticated = (u1[:16] == U[:16]) if not is_authenticated: raise ADEPTError('Password is not correct.') self.decrypt_key = key # genkey method if V == 1 or V == 2: self.genkey = self.genkey_v2 elif V == 3: self.genkey = self.genkey_v3 elif V == 4: self.genkey = self.genkey_v2 #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 # rc4 if V != 4: self.decipher = self.decipher_rc4 # XXX may be AES # aes elif V == 4 and length == 128: self.decipher = self.decipher_aes elif V == 4 and length == 256: raise PDFNotImplementedError('AES256 encryption is currently unsupported') self.ready = True return def initialize_ebx(self, password, docid, param): self.is_printable = self.is_modifiable = self.is_extractable = True rsa = RSA(password) length = int_value(param.get('Length', 0)) // 8 rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64') rights = zlib.decompress(rights, -15) rights = etree.fromstring(rights) expr = './/{http://ns.adobe.com/adept}encryptedKey' bookkey = codecs.decode(''.join(rights.findtext(expr)).encode('utf-8'),'base64') bookkey = rsa.decrypt(bookkey) #if bookkey[0] != 2: # raise ADEPTError('error decrypting book session key') if len(bookkey) > 16: if bookkey[-17] == '\x00' or bookkey[-17] == 0: bookkey = bookkey[-16:] length = 16 ebx_V = int_value(param.get('V', 4)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) # added because of improper booktype / decryption book session key errors if length > 0: if len(bookkey) == length: if ebx_V == 3: V = 3 else: V = 2 elif len(bookkey) == length + 1: V = bookkey[0] bookkey = bookkey[1:] else: print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("bookkey[0] is %d" % bookkey[0]) raise ADEPTError('error decrypting book session key - mismatched length') else: # proper length unknown try with whatever you have print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("bookkey[0] is %d" % bookkey[0]) if ebx_V == 3: V = 3 else: V = 2 self.decrypt_key = bookkey self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 self.decipher = self.decrypt_rc4 self.ready = True return # genkey functions def genkey_v2(self, objid, genno): objid = struct.pack(' PDFObjStmRef.maxindex: PDFObjStmRef.maxindex = index ## PDFParser ## class PDFParser(PSStackParser): def __init__(self, doc, fp): PSStackParser.__init__(self, fp) self.doc = doc self.doc.set_parser(self) return def __repr__(self): return '' KEYWORD_R = KWD(b'R') KEYWORD_ENDOBJ = KWD(b'endobj') KEYWORD_STREAM = KWD(b'stream') KEYWORD_XREF = KWD(b'xref') KEYWORD_STARTXREF = KWD(b'startxref') def do_keyword(self, pos, token): if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): self.add_results(*self.pop(1)) return if token is self.KEYWORD_ENDOBJ: self.add_results(*self.pop(4)) return if token is self.KEYWORD_R: # reference to indirect object try: ((_,objid), (_,genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass return if token is self.KEYWORD_STREAM: # stream object ((_,dic),) = self.pop(1) dic = dict_value(dic) try: objlen = int_value(dic['Length']) except KeyError: if STRICT: raise PDFSyntaxError('/Length is undefined: %r' % dic) objlen = 0 self.seek(pos) try: (_, line) = self.nextline() # 'stream' except PSEOF: if STRICT: raise PDFSyntaxError('Unexpected EOF') return pos += len(line) self.fp.seek(pos) data = self.fp.read(objlen) self.seek(pos+objlen) while 1: try: (linepos, line) = self.nextline() except PSEOF: if STRICT: raise PDFSyntaxError('Unexpected EOF') break if b'endstream' in line: i = line.index(b'endstream') objlen += i data += line[:i] break objlen += len(line) data += line self.seek(pos+objlen) obj = PDFStream(dic, data, self.doc.decipher) self.push((pos, obj)) return # others self.push((pos, token)) return def find_xref(self): # search the last xref table by scanning the file backwards. prev = None for line in self.revreadlines(): line = line.strip() if line == b'startxref': break if line: prev = line else: raise PDFNoValidXRef('Unexpected EOF') return int(prev) # read xref table def read_xref_from(self, start, xrefs): self.seek(start) self.reset() try: (pos, token) = self.nexttoken() except PSEOF: raise PDFNoValidXRef('Unexpected EOF') if isinstance(token, int): # XRefStream: PDF-1.5 if GEN_XREF_STM == 1: global gen_xref_stm gen_xref_stm = True self.seek(pos) self.reset() xref = PDFXRefStream() xref.load(self) else: if token is not self.KEYWORD_XREF: raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % (pos, token)) self.nextline() xref = PDFXRef() xref.load(self) xrefs.append(xref) trailer = xref.trailer if 'XRefStm' in trailer: pos = int_value(trailer['XRefStm']) self.read_xref_from(pos, xrefs) if 'Prev' in trailer: # find previous xref pos = int_value(trailer['Prev']) self.read_xref_from(pos, xrefs) return # read xref tables and trailers def read_xref(self): xrefs = [] trailerpos = None try: pos = self.find_xref() self.read_xref_from(pos, xrefs) except PDFNoValidXRef: # fallback self.seek(0) pat = re.compile(rb'^(\d+)\s+(\d+)\s+obj\b') offsets = {} xref = PDFXRef() while 1: try: (pos, line) = self.nextline() except PSEOF: break if line.startswith(b'trailer'): trailerpos = pos # remember last trailer m = pat.match(line) if not m: continue (objid, genno) = m.groups() offsets[int(objid)] = (0, pos) if not offsets: raise xref.offsets = offsets if trailerpos: self.seek(trailerpos) xref.load_trailer(self) xrefs.append(xref) return xrefs ## PDFObjStrmParser ## class PDFObjStrmParser(PDFParser): def __init__(self, data, doc): PSStackParser.__init__(self, BytesIO(data)) self.doc = doc return def flush(self): self.add_results(*self.popall()) return KEYWORD_R = KWD(b'R') def do_keyword(self, pos, token): if token is self.KEYWORD_R: # reference to indirect object try: ((_,objid), (_,genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass return # others self.push((pos, token)) return ### ### My own code, for which there is none else to blame class PDFSerializer(object): def __init__(self, inf, userkey): global GEN_XREF_STM, gen_xref_stm gen_xref_stm = GEN_XREF_STM > 1 self.version = inf.read(8) inf.seek(0) self.doc = doc = PDFDocument() parser = PDFParser(doc, inf) doc.initialize(userkey) self.objids = objids = set() for xref in reversed(doc.xrefs): trailer = xref.trailer for objid in xref.objids(): objids.add(objid) trailer = dict(trailer) trailer.pop('Prev', None) trailer.pop('XRefStm', None) if 'Encrypt' in trailer: objids.remove(trailer.pop('Encrypt').objid) self.trailer = trailer def dump(self, outf): self.outf = outf self.write(self.version) self.write(b'\n%\xe2\xe3\xcf\xd3\n') doc = self.doc objids = self.objids xrefs = {} maxobj = max(objids) trailer = dict(self.trailer) trailer['Size'] = maxobj + 1 for objid in objids: obj = doc.getobj(objid) if isinstance(obj, PDFObjStmRef): xrefs[objid] = obj continue if obj is not None: try: genno = obj.genno except AttributeError: genno = 0 xrefs[objid] = (self.tell(), genno) self.serialize_indirect(objid, obj) startxref = self.tell() if not gen_xref_stm: self.write(b'xref\n') self.write(b'0 %d\n' % (maxobj + 1,)) for objid in range(0, maxobj + 1): if objid in xrefs: # force the genno to be 0 self.write(b"%010d 00000 n \n" % xrefs[objid][0]) else: self.write(b"%010d %05d f \n" % (0, 65535)) self.write(b'trailer\n') self.serialize_object(trailer) self.write(b'\nstartxref\n%d\n%%%%EOF' % startxref) else: # Generate crossref stream. # Calculate size of entries maxoffset = max(startxref, maxobj) maxindex = PDFObjStmRef.maxindex fl2 = 2 power = 65536 while maxoffset >= power: fl2 += 1 power *= 256 fl3 = 1 power = 256 while maxindex >= power: fl3 += 1 power *= 256 index = [] first = None prev = None data = [] # Put the xrefstream's reference in itself startxref = self.tell() maxobj += 1 xrefs[maxobj] = (startxref, 0) for objid in sorted(xrefs): if first is None: first = objid elif objid != prev + 1: index.extend((first, prev - first + 1)) first = objid prev = objid objref = xrefs[objid] if isinstance(objref, PDFObjStmRef): f1 = 2 f2 = objref.stmid f3 = objref.index else: f1 = 1 f2 = objref[0] # we force all generation numbers to be 0 # f3 = objref[1] f3 = 0 data.append(struct.pack('>B', f1)) data.append(struct.pack('>L', f2)[-fl2:]) data.append(struct.pack('>L', f3)[-fl3:]) index.extend((first, prev - first + 1)) data = zlib.compress(b''.join(data)) dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, 'W': [1, fl2, fl3], 'Length': len(data), 'Filter': LITERALS_FLATE_DECODE[0], 'Root': trailer['Root'],} if 'Info' in trailer: dic['Info'] = trailer['Info'] xrefstm = PDFStream(dic, data) self.serialize_indirect(maxobj, xrefstm) self.write(b'startxref\n%d\n%%%%EOF' % startxref) def write(self, data): self.outf.write(data) self.last = data[-1:] def tell(self): return self.outf.tell() def escape_string(self, string): string = string.replace(b'\\', b'\\\\') string = string.replace(b'\n', rb'\n') string = string.replace(b'(', rb'\(') string = string.replace(b')', rb'\)') return string def serialize_object(self, obj): if isinstance(obj, dict): # Correct malformed Mac OS resource forks for Stanza if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ and isinstance(obj['Type'], int): obj['Subtype'] = obj['Type'] del obj['Type'] # end - hope this doesn't have bad effects self.write(b'<<') for key, val in obj.items(): self.write(str(LIT(key.encode('utf-8'))).encode('utf-8')) self.serialize_object(val) self.write(b'>>') elif isinstance(obj, list): self.write(b'[') for val in obj: self.serialize_object(val) self.write(b']') elif isinstance(obj, bytearray): self.write(b'(%s)' % self.escape_string(obj)) elif isinstance(obj, bytes): self.write(b'(%s)' % self.escape_string(obj)) elif isinstance(obj, str): self.write(b'(%s)' % self.escape_string(obj.encode('utf-8'))) elif isinstance(obj, bool): if self.last.isalnum(): self.write(b' ') self.write(str(obj).lower().encode('utf-8')) elif isinstance(obj, int): if self.last.isalnum(): self.write(b' ') self.write(str(obj).encode('utf-8')) elif isinstance(obj, Decimal): if self.last.isalnum(): self.write(b' ') self.write(str(obj).encode('utf-8')) elif isinstance(obj, PDFObjRef): if self.last.isalnum(): self.write(b' ') self.write(b'%d %d R' % (obj.objid, 0)) elif isinstance(obj, PDFStream): ### If we don't generate cross ref streams the object streams ### are no longer useful, as we have extracted all objects from ### them. Therefore leave them out from the output. if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: self.write('(deleted)') else: data = obj.get_decdata() self.serialize_object(obj.dic) self.write(b'stream\n') self.write(data) self.write(b'\nendstream') else: data = str(obj).encode('utf-8') if bytes([data[0]]).isalnum() and self.last.isalnum(): self.write(b' ') self.write(data) def serialize_indirect(self, objid, obj): self.write(b'%d 0 obj' % (objid,)) self.serialize_object(obj) if self.last.isalnum(): self.write(b'\n') self.write(b'endobj\n') def decryptBook(userkey, inpath, outpath): if RSA is None: raise ADEPTError("PyCryptodome or OpenSSL must be installed.") with open(inpath, 'rb') as inf: serializer = PDFSerializer(inf, userkey) with open(outpath, 'wb') as outf: # help construct to make sure the method runs to the end try: serializer.dump(outf) except Exception as e: print("error writing pdf: {0}".format(e)) return 2 return 0 def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv) != 4: print("usage: {0} ".format(progname)) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) if result == 0: print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) return result def gui_main(): try: import tkinter import tkinter.constants import tkinter.filedialog import tkinter.messagebox import traceback except: return cli_main() class DecryptionDialog(tkinter.Frame): def __init__(self, root): tkinter.Frame.__init__(self, root, border=5) self.status = tkinter.Label(self, text="Select files for decryption") self.status.pack(fill=tkinter.constants.X, expand=1) body = tkinter.Frame(self) body.pack(fill=tkinter.constants.X, expand=1) sticky = tkinter.constants.E + tkinter.constants.W body.grid_columnconfigure(1, weight=2) tkinter.Label(body, text="Key file").grid(row=0) self.keypath = tkinter.Entry(body, width=30) self.keypath.grid(row=0, column=1, sticky=sticky) if os.path.exists("adeptkey.der"): self.keypath.insert(0, "adeptkey.der") button = tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) tkinter.Label(body, text="Input file").grid(row=1) self.inpath = tkinter.Entry(body, width=30) self.inpath.grid(row=1, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_inpath) button.grid(row=1, column=2) tkinter.Label(body, text="Output file").grid(row=2) self.outpath = tkinter.Entry(body, width=30) self.outpath.grid(row=2, column=1, sticky=sticky) button = tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=2, column=2) buttons = tkinter.Frame(self) buttons.pack() botton = tkinter.Button( buttons, text="Decrypt", width=10, command=self.decrypt) botton.pack(side=tkinter.constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) button = tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=tkinter.constants.RIGHT) def get_keypath(self): keypath = tkinter.filedialog.askopenfilename( parent=None, title="Select Adobe Adept \'.der\' key file", defaultextension=".der", filetypes=[('Adobe Adept DER-encoded files', '.der'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, tkinter.constants.END) self.keypath.insert(0, keypath) return def get_inpath(self): inpath = tkinter.filedialog.askopenfilename( parent=None, title="Select ADEPT-encrypted PDF file to decrypt", defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, tkinter.constants.END) self.inpath.insert(0, inpath) return def get_outpath(self): outpath = tkinter.filedialog.asksaveasfilename( parent=None, title="Select unencrypted PDF file to produce", defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, tkinter.constants.END) self.outpath.insert(0, outpath) return def decrypt(self): keypath = self.keypath.get() inpath = self.inpath.get() outpath = self.outpath.get() if not keypath or not os.path.exists(keypath): self.status['text'] = "Specified key file does not exist" return if not inpath or not os.path.exists(inpath): self.status['text'] = "Specified input file does not exist" return if not outpath: self.status['text'] = "Output file not specified" return if inpath == outpath: self.status['text'] = "Must have different input and output files" return userkey = open(keypath,'rb').read() self.status['text'] = "Decrypting..." try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception as e: self.status['text'] = "Error; {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = "File successfully decrypted" else: self.status['text'] = "The was an error decrypting the file." root = tkinter.Tk() if RSA is None: root.withdraw() tkinter.messagebox.showerror( "INEPT PDF", "This script requires OpenSSL or PyCrypto, which must be installed " "separately. Read the top-of-script comment for details.") return 1 root.title("Adobe Adept PDF Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(370, 0) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/ion.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ion.py # Copyright © 2013-2020 Apprentice Harper et al. __license__ = 'GPL v3' __version__ = '3.0' # Revision history: # Pascal implementation by lulzkabulz. # BinaryIon.pas + DrmIon.pas + IonSymbols.pas # 1.0 - Python translation by apprenticenaomi. # 1.1 - DeDRM integration by anon. # 1.2 - Added pylzma import fallback # 1.3 - Fixed lzma support for calibre 4.6+ # 2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya. # 3.0 - Added Python 3 compatibility for calibre 5.0 """ Decrypt Kindle KFX files. """ import collections import hashlib import hmac import os import os.path import struct from io import BytesIO from Crypto.Cipher import AES from Crypto.Util.py3compat import bchr try: # lzma library from calibre 4.6.0 or later import calibre_lzma.lzma1 as calibre_lzma except ImportError: calibre_lzma = None # lzma library from calibre 2.35.0 or later try: import lzma.lzma1 as calibre_lzma except ImportError: calibre_lzma = None try: import lzma except ImportError: # Need pip backports.lzma on Python <3.3 try: from backports import lzma except ImportError: # Windows-friendly choice: pylzma wheels import pylzma as lzma TID_NULL = 0 TID_BOOLEAN = 1 TID_POSINT = 2 TID_NEGINT = 3 TID_FLOAT = 4 TID_DECIMAL = 5 TID_TIMESTAMP = 6 TID_SYMBOL = 7 TID_STRING = 8 TID_CLOB = 9 TID_BLOB = 0xA TID_LIST = 0xB TID_SEXP = 0xC TID_STRUCT = 0xD TID_TYPEDECL = 0xE TID_UNUSED = 0xF SID_UNKNOWN = -1 SID_ION = 1 SID_ION_1_0 = 2 SID_ION_SYMBOL_TABLE = 3 SID_NAME = 4 SID_VERSION = 5 SID_IMPORTS = 6 SID_SYMBOLS = 7 SID_MAX_ID = 8 SID_ION_SHARED_SYMBOL_TABLE = 9 SID_ION_1_0_MAX = 10 LEN_IS_VAR_LEN = 0xE LEN_IS_NULL = 0xF VERSION_MARKER = [b"\x01", b"\x00", b"\xEA"] # asserts must always raise exceptions for proper functioning def _assert(test, msg="Exception"): if not test: raise Exception(msg) class SystemSymbols(object): ION = '$ion' ION_1_0 = '$ion_1_0' ION_SYMBOL_TABLE = '$ion_symbol_table' NAME = 'name' VERSION = 'version' IMPORTS = 'imports' SYMBOLS = 'symbols' MAX_ID = 'max_id' ION_SHARED_SYMBOL_TABLE = '$ion_shared_symbol_table' class IonCatalogItem(object): name = "" version = 0 symnames = [] def __init__(self, name, version, symnames): self.name = name self.version = version self.symnames = symnames class SymbolToken(object): text = "" sid = 0 def __init__(self, text, sid): if text == "" and sid == 0: raise ValueError("Symbol token must have Text or SID") self.text = text self.sid = sid class SymbolTable(object): table = None def __init__(self): self.table = [None] * SID_ION_1_0_MAX self.table[SID_ION] = SystemSymbols.ION self.table[SID_ION_1_0] = SystemSymbols.ION_1_0 self.table[SID_ION_SYMBOL_TABLE] = SystemSymbols.ION_SYMBOL_TABLE self.table[SID_NAME] = SystemSymbols.NAME self.table[SID_VERSION] = SystemSymbols.VERSION self.table[SID_IMPORTS] = SystemSymbols.IMPORTS self.table[SID_SYMBOLS] = SystemSymbols.SYMBOLS self.table[SID_MAX_ID] = SystemSymbols.MAX_ID self.table[SID_ION_SHARED_SYMBOL_TABLE] = SystemSymbols.ION_SHARED_SYMBOL_TABLE def findbyid(self, sid): if sid < 1: raise ValueError("Invalid symbol id") if sid < len(self.table): return self.table[sid] else: return "" def import_(self, table, maxid): for i in range(maxid): self.table.append(table.symnames[i]) def importunknown(self, name, maxid): for i in range(maxid): self.table.append("%s#%d" % (name, i + 1)) class ParserState: Invalid,BeforeField,BeforeTID,BeforeValue,AfterValue,EOF = 1,2,3,4,5,6 ContainerRec = collections.namedtuple("ContainerRec", "nextpos, tid, remaining") class BinaryIonParser(object): eof = False state = None localremaining = 0 needhasnext = False isinstruct = False valuetid = 0 valuefieldid = 0 parenttid = 0 valuelen = 0 valueisnull = False valueistrue = False value = None didimports = False def __init__(self, stream): self.annotations = [] self.catalog = [] self.stream = stream self.initpos = stream.tell() self.reset() self.symbols = SymbolTable() def reset(self): self.state = ParserState.BeforeTID self.needhasnext = True self.localremaining = -1 self.eof = False self.isinstruct = False self.containerstack = [] self.stream.seek(self.initpos) def addtocatalog(self, name, version, symbols): self.catalog.append(IonCatalogItem(name, version, symbols)) def hasnext(self): while self.needhasnext and not self.eof: self.hasnextraw() if len(self.containerstack) == 0 and not self.valueisnull: if self.valuetid == TID_SYMBOL: if self.value == SID_ION_1_0: self.needhasnext = True elif self.valuetid == TID_STRUCT: for a in self.annotations: if a == SID_ION_SYMBOL_TABLE: self.parsesymboltable() self.needhasnext = True break return not self.eof def hasnextraw(self): self.clearvalue() while self.valuetid == -1 and not self.eof: self.needhasnext = False if self.state == ParserState.BeforeField: _assert(self.valuefieldid == SID_UNKNOWN) self.valuefieldid = self.readfieldid() if self.valuefieldid != SID_UNKNOWN: self.state = ParserState.BeforeTID else: self.eof = True elif self.state == ParserState.BeforeTID: self.state = ParserState.BeforeValue self.valuetid = self.readtypeid() if self.valuetid == -1: self.state = ParserState.EOF self.eof = True break if self.valuetid == TID_TYPEDECL: if self.valuelen == 0: self.checkversionmarker() else: self.loadannotations() elif self.state == ParserState.BeforeValue: self.skip(self.valuelen) self.state = ParserState.AfterValue elif self.state == ParserState.AfterValue: if self.isinstruct: self.state = ParserState.BeforeField else: self.state = ParserState.BeforeTID else: _assert(self.state == ParserState.EOF) def next(self): if self.hasnext(): self.needhasnext = True return self.valuetid else: return -1 def push(self, typeid, nextposition, nextremaining): self.containerstack.append(ContainerRec(nextpos=nextposition, tid=typeid, remaining=nextremaining)) def stepin(self): _assert(self.valuetid in [TID_STRUCT, TID_LIST, TID_SEXP] and not self.eof, "valuetid=%s eof=%s" % (self.valuetid, self.eof)) _assert((not self.valueisnull or self.state == ParserState.AfterValue) and (self.valueisnull or self.state == ParserState.BeforeValue)) nextrem = self.localremaining if nextrem != -1: nextrem -= self.valuelen if nextrem < 0: nextrem = 0 self.push(self.parenttid, self.stream.tell() + self.valuelen, nextrem) self.isinstruct = (self.valuetid == TID_STRUCT) if self.isinstruct: self.state = ParserState.BeforeField else: self.state = ParserState.BeforeTID self.localremaining = self.valuelen self.parenttid = self.valuetid self.clearvalue() self.needhasnext = True def stepout(self): rec = self.containerstack.pop() self.eof = False self.parenttid = rec.tid if self.parenttid == TID_STRUCT: self.isinstruct = True self.state = ParserState.BeforeField else: self.isinstruct = False self.state = ParserState.BeforeTID self.needhasnext = True self.clearvalue() curpos = self.stream.tell() if rec.nextpos > curpos: self.skip(rec.nextpos - curpos) else: _assert(rec.nextpos == curpos) self.localremaining = rec.remaining def read(self, count=1): if self.localremaining != -1: self.localremaining -= count _assert(self.localremaining >= 0) result = self.stream.read(count) if len(result) == 0: raise EOFError() return result def readfieldid(self): if self.localremaining != -1 and self.localremaining < 1: return -1 try: return self.readvaruint() except EOFError: return -1 def readtypeid(self): if self.localremaining != -1: if self.localremaining < 1: return -1 self.localremaining -= 1 b = self.stream.read(1) if len(b) < 1: return -1 b = ord(b) result = b >> 4 ln = b & 0xF if ln == LEN_IS_VAR_LEN: ln = self.readvaruint() elif ln == LEN_IS_NULL: ln = 0 self.state = ParserState.AfterValue elif result == TID_NULL: # Must have LEN_IS_NULL _assert(False) elif result == TID_BOOLEAN: _assert(ln <= 1) self.valueistrue = (ln == 1) ln = 0 self.state = ParserState.AfterValue elif result == TID_STRUCT: if ln == 1: ln = self.readvaruint() self.valuelen = ln return result def readvarint(self): b = ord(self.read()) negative = ((b & 0x40) != 0) result = (b & 0x3F) i = 0 while (b & 0x80) == 0 and i < 4: b = ord(self.read()) result = (result << 7) | (b & 0x7F) i += 1 _assert(i < 4 or (b & 0x80) != 0, "int overflow") if negative: return -result return result def readvaruint(self): b = ord(self.read()) result = (b & 0x7F) i = 0 while (b & 0x80) == 0 and i < 4: b = ord(self.read()) result = (result << 7) | (b & 0x7F) i += 1 _assert(i < 4 or (b & 0x80) != 0, "int overflow") return result def readdecimal(self): if self.valuelen == 0: return 0. rem = self.localremaining - self.valuelen self.localremaining = self.valuelen exponent = self.readvarint() _assert(self.localremaining > 0, "Only exponent in ReadDecimal") _assert(self.localremaining <= 8, "Decimal overflow") signed = False b = [ord(x) for x in self.read(self.localremaining)] if (b[0] & 0x80) != 0: b[0] = b[0] & 0x7F signed = True # Convert variably sized network order integer into 64-bit little endian j = 0 vb = [0] * 8 for i in range(len(b), -1, -1): vb[i] = b[j] j += 1 v = struct.unpack(" 0: result = result[:-1] return result def ionwalk(self, supert, indent, lst): while self.hasnext(): if supert == TID_STRUCT: L = self.getfieldname() + ":" else: L = "" t = self.next() if t in [TID_STRUCT, TID_LIST]: if L != "": lst.append(indent + L) L = self.gettypename() if L != "": lst.append(indent + L + "::") if t == TID_STRUCT: lst.append(indent + "{") else: lst.append(indent + "[") self.stepin() self.ionwalk(t, indent + " ", lst) self.stepout() if t == TID_STRUCT: lst.append(indent + "}") else: lst.append(indent + "]") else: if t == TID_STRING: L += ('"%s"' % self.stringvalue()) elif t in [TID_CLOB, TID_BLOB]: L += ("{%s}" % self.printlob(self.lobvalue())) elif t == TID_POSINT: L += str(self.intvalue()) elif t == TID_SYMBOL: tn = self.gettypename() if tn != "": tn += "::" L += tn + self.symbolvalue() elif t == TID_DECIMAL: L += str(self.decimalvalue()) else: L += ("TID %d" % t) lst.append(indent + L) def print_(self, lst): self.reset() self.ionwalk(-1, "", lst) SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0', 'com.amazon.drm.EnvelopeMetadata@1.0', 'size', 'page_size', 'encryption_key', 'encryption_transformation', 'encryption_voucher', 'signing_key', 'signing_algorithm', 'signing_voucher', 'com.amazon.drm.EncryptedPage@1.0', 'cipher_text', 'cipher_iv', 'com.amazon.drm.Signature@1.0', 'data', 'com.amazon.drm.EnvelopeIndexTable@1.0', 'length', 'offset', 'algorithm', 'encoded', 'encryption_algorithm', 'hashing_algorithm', 'expires', 'format', 'id', 'lock_parameters', 'strategy', 'com.amazon.drm.Key@1.0', 'com.amazon.drm.KeySet@1.0', 'com.amazon.drm.PIDv3@1.0', 'com.amazon.drm.PlainTextPage@1.0', 'com.amazon.drm.PlainText@1.0', 'com.amazon.drm.PrivateKey@1.0', 'com.amazon.drm.PublicKey@1.0', 'com.amazon.drm.SecretKey@1.0', 'com.amazon.drm.Voucher@1.0', 'public_key', 'private_key', 'com.amazon.drm.KeyPair@1.0', 'com.amazon.drm.ProtectedData@1.0', 'doctype', 'com.amazon.drm.EnvelopeIndexTableOffset@1.0', 'enddoc', 'license_type', 'license', 'watermark', 'key', 'value', 'com.amazon.drm.License@1.0', 'category', 'metadata', 'categorized_metadata', 'com.amazon.drm.CategorizedMetadata@1.0', 'com.amazon.drm.VoucherEnvelope@1.0', 'mac', 'voucher', 'com.amazon.drm.ProtectedData@2.0', 'com.amazon.drm.Envelope@2.0', 'com.amazon.drm.EnvelopeMetadata@2.0', 'com.amazon.drm.EncryptedPage@2.0', 'com.amazon.drm.PlainText@2.0', 'compression_algorithm', 'com.amazon.drm.Compressed@1.0', 'page_index_table', ] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n for n in list(range(2, 29)) + [ 9708, 1031, 2069, 9041, 3646, 6052, 9479, 9888, 4648, 5683]] def addprottable(ion): ion.addtocatalog("ProtectedData", 1, SYM_NAMES) def pkcs7pad(msg, blocklen): paddinglen = blocklen - len(msg) % blocklen padding = bchr(paddinglen) * paddinglen return msg + padding def pkcs7unpad(msg, blocklen): _assert(len(msg) % blocklen == 0) paddinglen = msg[-1] _assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key") _assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key") return msg[:-paddinglen] # every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret OBFUSCATION_TABLE = { "V1": (0x00, None), "V2": (0x05, b'Antidisestablishmentarianism'), "V3": (0x08, b'Floccinaucinihilipilification'), "V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'), "V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'), "V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'), "V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'), "V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"), "V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'), "V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'), "V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'), "V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'), "V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'), "V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"), "V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'), "V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'), "V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'), "V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e,~hZ\x12jK\x17\x1e*1'), "V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'), "V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'), "V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'), "V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'), "V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"), "V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'), "V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'), } # obfuscate shared secret according to the VoucherEnvelope version def obfuscate(secret, version): if version == 1: # v1 does not use obfuscation return secret magic, word = OBFUSCATION_TABLE["V%d" % version] # extend secret so that its length is divisible by the magic number if len(secret) % magic != 0: secret = secret + b'\x00' * (magic - len(secret) % magic) secret = bytearray(secret) obfuscated = bytearray(len(secret)) wordhash = bytearray(hashlib.sha256(word).digest()) # shuffle secret and xor it with the first half of the word hash for i in range(0, len(secret)): index = i // (len(secret) // magic) + magic * (i % (len(secret) // magic)) obfuscated[index] = secret[i] ^ wordhash[index % 16] return obfuscated class DrmIonVoucher(object): envelope = None version = None voucher = None drmkey = None license_type = "Unknown" encalgorithm = "" enctransformation = "" hashalgorithm = "" lockparams = None ciphertext = b"" cipheriv = b"" secretkey = b"" def __init__(self, voucherenv, dsn, secret): self.dsn, self.secret = dsn, secret if isinstance(dsn, str): self.dsn = dsn.encode('ASCII') if isinstance(secret, str): self.secret = secret.encode('ASCII') self.lockparams = [] self.envelope = BinaryIonParser(voucherenv) addprottable(self.envelope) def decryptvoucher(self): shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII') self.lockparams.sort() for param in self.lockparams: if param == "ACCOUNT_SECRET": shared += param.encode('ASCII') + self.secret elif param == "CLIENT_ID": shared += param.encode('ASCII') + self.dsn else: _assert(False, "Unknown lock parameter: %s" % param) sharedsecret = obfuscate(shared, self.version) key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest() aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16]) b = aes.decrypt(self.ciphertext) b = pkcs7unpad(b, 16) self.drmkey = BinaryIonParser(BytesIO(b)) addprottable(self.drmkey) _assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0", "Expected KeySet, got %s" % self.drmkey.gettypename()) self.drmkey.stepin() while self.drmkey.hasnext(): self.drmkey.next() if self.drmkey.gettypename() != "com.amazon.drm.SecretKey@1.0": continue self.drmkey.stepin() while self.drmkey.hasnext(): self.drmkey.next() if self.drmkey.getfieldname() == "algorithm": _assert(self.drmkey.stringvalue() == "AES", "Unknown cipher algorithm: %s" % self.drmkey.stringvalue()) elif self.drmkey.getfieldname() == "format": _assert(self.drmkey.stringvalue() == "RAW", "Unknown key format: %s" % self.drmkey.stringvalue()) elif self.drmkey.getfieldname() == "encoded": self.secretkey = self.drmkey.lobvalue() self.drmkey.stepout() break self.drmkey.stepout() def parse(self): self.envelope.reset() _assert(self.envelope.hasnext(), "Envelope is empty") _assert(self.envelope.next() == TID_STRUCT and str.startswith(self.envelope.gettypename(), "com.amazon.drm.VoucherEnvelope@"), "Unknown type encountered in envelope, expected VoucherEnvelope") self.version = int(self.envelope.gettypename().split('@')[1][:-2]) self.envelope.stepin() while self.envelope.hasnext(): self.envelope.next() field = self.envelope.getfieldname() if field == "voucher": self.voucher = BinaryIonParser(BytesIO(self.envelope.lobvalue())) addprottable(self.voucher) continue elif field != "strategy": continue _assert(self.envelope.gettypename() == "com.amazon.drm.PIDv3@1.0", "Unknown strategy: %s" % self.envelope.gettypename()) self.envelope.stepin() while self.envelope.hasnext(): self.envelope.next() field = self.envelope.getfieldname() if field == "encryption_algorithm": self.encalgorithm = self.envelope.stringvalue() elif field == "encryption_transformation": self.enctransformation = self.envelope.stringvalue() elif field == "hashing_algorithm": self.hashalgorithm = self.envelope.stringvalue() elif field == "lock_parameters": self.envelope.stepin() while self.envelope.hasnext(): _assert(self.envelope.next() == TID_STRING, "Expected string list for lock_parameters") self.lockparams.append(self.envelope.stringvalue()) self.envelope.stepout() self.envelope.stepout() self.parsevoucher() def parsevoucher(self): _assert(self.voucher.hasnext(), "Voucher is empty") _assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0", "Unknown type, expected Voucher") self.voucher.stepin() while self.voucher.hasnext(): self.voucher.next() if self.voucher.getfieldname() == "cipher_iv": self.cipheriv = self.voucher.lobvalue() elif self.voucher.getfieldname() == "cipher_text": self.ciphertext = self.voucher.lobvalue() elif self.voucher.getfieldname() == "license": _assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0", "Unknown license: %s" % self.voucher.gettypename()) self.voucher.stepin() while self.voucher.hasnext(): self.voucher.next() if self.voucher.getfieldname() == "license_type": self.license_type = self.voucher.stringvalue() self.voucher.stepout() def printenvelope(self, lst): self.envelope.print_(lst) def printkey(self, lst): if self.voucher is None: self.parse() if self.drmkey is None: self.decryptvoucher() self.drmkey.print_(lst) def printvoucher(self, lst): if self.voucher is None: self.parse() self.voucher.print_(lst) def getlicensetype(self): return self.license_type class DrmIon(object): ion = None voucher = None vouchername = "" key = b"" onvoucherrequired = None def __init__(self, ionstream, onvoucherrequired): self.ion = BinaryIonParser(ionstream) addprottable(self.ion) self.onvoucherrequired = onvoucherrequired def parse(self, outpages): self.ion.reset() _assert(self.ion.hasnext(), "DRMION envelope is empty") _assert(self.ion.next() == TID_SYMBOL and self.ion.gettypename() == "doctype", "Expected doctype symbol") _assert(self.ion.next() == TID_LIST and self.ion.gettypename() in ["com.amazon.drm.Envelope@1.0", "com.amazon.drm.Envelope@2.0"], "Unknown type encountered in DRMION envelope, expected Envelope, got %s" % self.ion.gettypename()) while True: if self.ion.gettypename() == "enddoc": break self.ion.stepin() while self.ion.hasnext(): self.ion.next() if self.ion.gettypename() in ["com.amazon.drm.EnvelopeMetadata@1.0", "com.amazon.drm.EnvelopeMetadata@2.0"]: self.ion.stepin() while self.ion.hasnext(): self.ion.next() if self.ion.getfieldname() != "encryption_voucher": continue if self.vouchername == "": self.vouchername = self.ion.stringvalue() self.voucher = self.onvoucherrequired(self.vouchername) self.key = self.voucher.secretkey _assert(self.key is not None, "Unable to obtain secret key from voucher") else: _assert(self.vouchername == self.ion.stringvalue(), "Unexpected: Different vouchers required for same file?") self.ion.stepout() elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]: decompress = False decrypt = True ct = None civ = None self.ion.stepin() while self.ion.hasnext(): self.ion.next() if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0": decompress = True if self.ion.getfieldname() == "cipher_text": ct = self.ion.lobvalue() elif self.ion.getfieldname() == "cipher_iv": civ = self.ion.lobvalue() if ct is not None and civ is not None: self.processpage(ct, civ, outpages, decompress, decrypt) self.ion.stepout() elif self.ion.gettypename() in ["com.amazon.drm.PlainText@1.0", "com.amazon.drm.PlainText@2.0"]: decompress = False decrypt = False plaintext = None self.ion.stepin() while self.ion.hasnext(): self.ion.next() if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0": decompress = True if self.ion.getfieldname() == "data": plaintext = self.ion.lobvalue() if plaintext is not None: self.processpage(plaintext, None, outpages, decompress, decrypt) self.ion.stepout() self.ion.stepout() if not self.ion.hasnext(): break self.ion.next() def print_(self, lst): self.ion.print_(lst) def processpage(self, ct, civ, outpages, decompress, decrypt): if decrypt: aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) msg = pkcs7unpad(aes.decrypt(ct), 16) else: msg = ct if not decompress: outpages.write(msg) return _assert(msg[0] == 0, "LZMA UseFilter not supported") if calibre_lzma is not None: with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f: f.seek(0) outpages.write(f.read()) return decomp = lzma.LZMADecompressor(format=lzma.FORMAT_ALONE) while not decomp.eof: segment = decomp.decompress(msg[1:]) msg = b"" # Contents were internally buffered after the first call outpages.write(segment) ================================================ FILE: DeDRM_plugin/k4mobidedrm.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # k4mobidedrm.py # Copyright © 2008-2020 by Apprentice Harper et al. __license__ = 'GPL v3' __version__ = '6.0' # Engine to remove drm from Kindle and Mobipocket ebooks # for personal use for archiving and converting your ebooks # PLEASE DO NOT PIRATE EBOOKS! # We want all authors and publishers, and ebook stores to live # long and prosperous lives but at the same time we just want to # be able to read OUR books on whatever device we want and to keep # readable for a long, long time # This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle, # unswindle, DarkReverser, ApprenticeAlf, and many many others # Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump # from which this script borrows most unashamedly. # Changelog # 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code # 1.1 - Adds support for additional kindle.info files # 1.2 - Better error handling for older Mobipocket # 1.3 - Don't try to decrypt Topaz books # 1.7 - Add support for Topaz books and Kindle serial numbers. Split code. # 1.9 - Tidy up after Topaz, minor exception changes # 2.1 - Topaz fix and filename sanitizing # 2.2 - Topaz Fix and minor Mac code fix # 2.3 - More Topaz fixes # 2.4 - K4PC/Mac key generation fix # 2.6 - Better handling of non-K4PC/Mac ebooks # 2.7 - Better trailing bytes handling in mobidedrm # 2.8 - Moved parsing of kindle.info files to mac & pc util files. # 3.1 - Updated for new calibre interface. Now __init__ in plugin. # 3.5 - Now support Kindle for PC/Mac 1.6 # 3.6 - Even better trailing bytes handling in mobidedrm # 3.7 - Add support for Amazon Print Replica ebooks. # 3.8 - Improved Topaz support # 4.1 - Improved Topaz support and faster decryption with alfcrypto # 4.2 - Added support for Amazon's KF8 format ebooks # 4.4 - Linux calls to Wine added, and improved configuration dialog # 4.5 - Linux works again without Wine. Some Mac key file search changes # 4.6 - First attempt to handle unicode properly # 4.7 - Added timing reports, and changed search for Mac key files # 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts # - Moved back into plugin, __init__ in plugin now only contains plugin code. # 4.9 - Missed some invalid characters in cleanup_name # 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py # - tweaked GetDecryptedBook interface to leave passed parameters unchanged # 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility # 5.2 - Fixed error in command line processing of unicode arguments # 5.3 - Changed Android support to allow passing of backup .ab files # 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet. # 5.5 - Added GPL v3 licence explicitly. # 5.6 - Invoke KFXZipBook to handle zipped KFX files # 5.7 - Revamp cleanup_name # 6.0 - Added Python 3 compatibility for calibre 5.0 import sys, os, re import csv import getopt import re import traceback import time import html.entities import json class DrmException(Exception): pass if 'calibre' in sys.modules: inCalibre = True else: inCalibre = False if inCalibre: from calibre_plugins.dedrm import mobidedrm from calibre_plugins.dedrm import topazextract from calibre_plugins.dedrm import kgenpids from calibre_plugins.dedrm import androidkindlekey from calibre_plugins.dedrm import kfxdedrm else: import mobidedrm import topazextract import kgenpids import androidkindlekey import kfxdedrm # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["mobidedrm.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] # cleanup unicode filenames # borrowed from calibre from calibre/src/calibre/__init__.py # added in removal of control (<32) chars # and removal of . at start and end # and with some (heavily edited) code from Paul Durrant's kindlenamer.py # and some improvements suggested by jhaisley def cleanup_name(name): # substitute filename unfriendly characters name = name.replace("<","[").replace(">","]").replace(" : "," – ").replace(": "," – ").replace(":","—").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","") # white space to single space, delete leading and trailing while space name = re.sub(r"\s", " ", name).strip() # delete control characters name = "".join(char for char in name if ord(char)>=32) # delete non-ascii characters name = "".join(char for char in name if ord(char)<=126) # remove leading dots while len(name)>0 and name[0] == ".": name = name[1:] # remove trailing dots (Windows doesn't like them) while name.endswith("."): name = name[:-1] if len(name)==0: name="DecryptedBook" return name # must be passed unicode def unescape(text): def fixup(m): text = m.group(0) if text[:2] == "&#": # character reference try: if text[:3] == "&#x": return chr(int(text[3:-1], 16)) else: return chr(int(text[2:-1])) except ValueError: pass else: # named entity try: text = chr(html.entities.name2codepoint[text[1:-1]]) except KeyError: pass return text # leave as is return re.sub("&#?\\w+;", fixup, text) def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()): # handle the obvious cases at the beginning if not os.path.isfile(infile): raise DrmException("Input file does not exist.") mobi = True magic8 = open(infile,'rb').read(8) if magic8 == b'\xeaDRMION\xee': raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.") magic3 = magic8[:3] if magic3 == b'TPZ': mobi = False if magic8[:4] == b'PK\x03\x04': mb = kfxdedrm.KFXZipBook(infile) elif mobi: mb = mobidedrm.MobiBook(infile) else: mb = topazextract.TopazBook(infile) bookname = unescape(mb.getBookTitle()) print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())) # copy list of pids totalpids = list(pids) # extend list of serials with serials from android databases for aFile in androidFiles: serials.extend(androidkindlekey.get_serials(aFile)) # extend PID list with book-specific PIDs from seriala and kDatabases md1, md2 = mb.getPIDMetaInfo() totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) # remove any duplicates totalpids = list(set(totalpids)) print("Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))) #print totalpids try: mb.processBook(totalpids) except: mb.cleanup raise print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)) return mb # kDatabaseFiles is a list of files created by kindlekey def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids): starttime = time.time() kDatabases = [] for dbfile in kDatabaseFiles: kindleDatabase = {} try: with open(dbfile, 'r') as keyfilein: kindleDatabase = json.loads(keyfilein.read()) kDatabases.append([dbfile,kindleDatabase]) except Exception as e: print("Error getting database from file {0:s}: {1:s}".format(dbfile,e)) traceback.print_exc() try: book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime) except Exception as e: print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)) traceback.print_exc() return 1 # Try to infer a reasonable name orig_fn_root = os.path.splitext(os.path.basename(infile))[0] if ( re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or re.match('^{0-9A-F-}{36}$', orig_fn_root) ): # Kindle for PC / Mac / Android / Fire / iOS clean_title = cleanup_name(book.getBookTitle()) outfilename = "{}_{}".format(orig_fn_root, clean_title) else: # E Ink Kindle, which already uses a reasonable name outfilename = orig_fn_root # avoid excessively long file names if len(outfilename)>150: outfilename = outfilename[:99]+"--"+outfilename[-49:] outfilename = outfilename+"_nodrm" outfile = os.path.join(outdir, outfilename + book.getBookExtension()) book.getFile(outfile) print("Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)) if book.getBookType()=="Topaz": zipname = os.path.join(outdir, outfilename + "_SVG.zip") book.getSVGZip(zipname) print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)) # remove internal temporary directory of Topaz pieces book.cleanup() return 0 def usage(progname): print("Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks") print("Usage:") print(" {0} [-k ] [-p ] [-s ] [ -a ] ".format(progname)) # # Main # def cli_main(): argv=unicode_argv() progname = os.path.basename(argv[0]) print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__)) try: opts, args = getopt.getopt(argv[1:], "k:p:s:a:h") except getopt.GetoptError as err: print("Error in options or arguments: {0}".format(err.args[0])) usage(progname) sys.exit(2) if len(args)<2: usage(progname) sys.exit(2) infile = args[0] outdir = args[1] kDatabaseFiles = [] androidFiles = [] serials = [] pids = [] for o, a in opts: if o == "-h": usage(progname) sys.exit(0) if o == "-k": if a == None : raise DrmException("Invalid parameter for -k") kDatabaseFiles.append(a) if o == "-p": if a == None : raise DrmException("Invalid parameter for -p") pids = a.encode('utf-8').split(b',') if o == "-s": if a == None : raise DrmException("Invalid parameter for -s") serials = a.split(',') if o == '-a': if a == None: raise DrmException("Invalid parameter for -a") androidFiles.append(a) return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids) if __name__ == '__main__': sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) sys.exit(cli_main()) ================================================ FILE: DeDRM_plugin/kfxdedrm.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # Engine to remove drm from Kindle KFX ebooks # 2.0 - Python 3 for calibre 5.0 # 2.1 - Some fixes for debugging # 2.1.1 - Whitespace! import os import shutil import traceback import zipfile from io import BytesIO try: from ion import DrmIon, DrmIonVoucher except: from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher __license__ = 'GPL v3' __version__ = '2.0' class KFXZipBook: def __init__(self, infile): self.infile = infile self.voucher = None self.decrypted = {} def getPIDMetaInfo(self): return (None, None) def processBook(self, totalpids): with zipfile.ZipFile(self.infile, 'r') as zf: for filename in zf.namelist(): with zf.open(filename) as fh: data = fh.read(8) if data != b'\xeaDRMION\xee': continue data += fh.read() if self.voucher is None: self.decrypt_voucher(totalpids) print("Decrypting KFX DRMION: {0}".format(filename)) outfile = BytesIO() DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile) self.decrypted[filename] = outfile.getvalue() if not self.decrypted: print("The .kfx-zip archive does not contain an encrypted DRMION file") def decrypt_voucher(self, totalpids): with zipfile.ZipFile(self.infile, 'r') as zf: for info in zf.infolist(): with zf.open(info.filename) as fh: data = fh.read(4) if data != b'\xe0\x01\x00\xea': continue data += fh.read() if b'ProtectedData' in data: break # found DRM voucher else: raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher") print("Decrypting KFX DRM voucher: {0}".format(info.filename)) for pid in [''] + totalpids: # Belt and braces. PIDs should be unicode strings, but just in case... if isinstance(pid, bytes): pid = pid.decode('ascii') for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]: if len(pid) == dsn_len + secret_len: break # split pid into DSN and account secret else: continue try: voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:]) voucher.parse() voucher.decryptvoucher() break except: traceback.print_exc() pass else: raise Exception("Failed to decrypt KFX DRM voucher with any key") print("KFX DRM voucher successfully decrypted") license_type = voucher.getlicensetype() if license_type != "Purchase": raise Exception(("This book is licensed as {0}. " 'These tools are intended for use on purchased books.').format(license_type)) self.voucher = voucher def getBookTitle(self): return os.path.splitext(os.path.split(self.infile)[1])[0] def getBookExtension(self): return '.kfx-zip' def getBookType(self): return 'KFX-ZIP' def cleanup(self): pass def getFile(self, outpath): if not self.decrypted: shutil.copyfile(self.infile, outpath) else: with zipfile.ZipFile(self.infile, 'r') as zif: with zipfile.ZipFile(outpath, 'w') as zof: for info in zif.infolist(): zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename))) ================================================ FILE: DeDRM_plugin/kgenpids.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # kgenpids.py # Copyright © 2008-2020 Apprentice Harper et al. __license__ = 'GPL v3' __version__ = '3.0' # Revision history: # 2.0 - Fix for non-ascii Windows user names # 2.1 - Actual fix for non-ascii WIndows user names. # 2.2 - Return information needed for KFX decryption # 3.0 - Python 3 for calibre 5.0 import sys import os, csv import binascii import zlib import re from struct import pack, unpack, unpack_from import traceback class DrmException(Exception): pass global charMap1 global charMap3 global charMap4 charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' charMap3 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' charMap4 = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' # crypto digestroutines import hashlib def MD5(message): ctx = hashlib.md5() ctx.update(message) return ctx.digest() def SHA1(message): ctx = hashlib.sha1() ctx.update(message) return ctx.digest() # Encode the bytes in data with the characters in map # data and map should be byte arrays def encode(data, map): result = b'' for char in data: value = char Q = (value ^ 0x80) // len(map) R = value % len(map) result += bytes([map[Q]]) result += bytes([map[R]]) return result # Hash the bytes in data and then encode the digest with the characters in map def encodeHash(data,map): return encode(MD5(data),map) # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): result = '' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low result += pack('B',value) return result # # PID generation routines # # Returns two bit at offset from a bit field def getTwoBitsFromBitField(bitField,offset): byteNumber = offset // 4 bitPosition = 6 - 2*(offset % 4) return bitField[byteNumber] >> bitPosition & 3 # Returns the six bits at offset from a bit field def getSixBitsFromBitField(bitField,offset): offset *= 3 value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) return value # 8 bits to six bits encoding from hash to generate PID string def encodePID(hash): global charMap3 PID = b'' for position in range (0,8): PID += bytes([charMap3[getSixBitsFromBitField(hash,position)]]) return PID # Encryption table used to generate the device PID def generatePidEncryptionTable() : table = [] for counter1 in range (0,0x100): value = counter1 for counter2 in range (0,8): if (value & 1 == 0) : value = value >> 1 else : value = value >> 1 value = value ^ 0xEDB88320 table.append(value) return table # Seed value used to generate the device PID def generatePidSeed(table,dsn) : value = 0 for counter in range (0,4) : index = (dsn[counter] ^ value) & 0xFF value = (value >> 8) ^ table[index] return value # Generate the device PID def generateDevicePID(table,dsn,nbRoll): global charMap4 seed = generatePidSeed(table,dsn) pidAscii = b'' pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] index = 0 for counter in range (0,nbRoll): pid[index] = pid[index] ^ dsn[counter] index = (index+1) %8 for counter in range (0,8): index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) pidAscii += bytes([charMap4[index]]) return pidAscii def crc32(s): return (~binascii.crc32(s,-1))&0xFFFFFFFF # convert from 8 digit PID to 10 digit PID with checksum def checksumPid(s): global charMap4 crc = crc32(s) crc = crc ^ (crc >> 16) res = s l = len(charMap4) for i in (0,1): b = crc & 0xff pos = (b // l) ^ (b % l) res += bytes([charMap4[pos%l]]) crc >>= 8 return res # old kindle serial number to fixed pid def pidFromSerial(s, l): global charMap4 crc = crc32(s) arr1 = [0]*l for i in range(len(s)): arr1[i%l] ^= s[i] crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] for i in range(l): arr1[i] ^= crc_bytes[i&3] pid = b"" for i in range(l): b = arr1[i] & 0xff pid += bytes([charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]]) return pid # Parse the EXTH header records and use the Kindle serial number to calculate the book pid. def getKindlePids(rec209, token, serialnum): if isinstance(serialnum,str): serialnum = serialnum.encode('utf-8') if rec209 is None: return [serialnum] pids=[] # Compute book PID pidHash = SHA1(serialnum+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) pids.append(bookPID) # compute fixed pid for old pre 2.5 firmware update pid as well kindlePID = pidFromSerial(serialnum, 7) + b"*" kindlePID = checksumPid(kindlePID) pids.append(kindlePID) return pids # parse the Kindleinfo file to calculate the book pid. keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber'] def getK4Pids(rec209, token, kindleDatabase): global charMap1 pids = [] try: # Get the kindle account token, if present kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens']) except KeyError: kindleAccountToken = b'' pass try: # Get the DSN token, if present DSN = bytearray.fromhex((kindleDatabase[1])['DSN']) print("Got DSN key from database {0}".format(kindleDatabase[0])) except KeyError: # See if we have the info to generate the DSN try: # Get the Mazama Random number MazamaRandomNumber = bytearray.fromhex((kindleDatabase[1])['MazamaRandomNumber']) #print "Got MazamaRandomNumber from database {0}".format(kindleDatabase[0]) try: # Get the SerialNumber token, if present IDString = bytearray.fromhex((kindleDatabase[1])['SerialNumber']) print("Got SerialNumber from database {0}".format(kindleDatabase[0])) except KeyError: # Get the IDString we added IDString = bytearray.fromhex((kindleDatabase[1])['IDString']) try: # Get the UsernameHash token, if present encodedUsername = bytearray.fromhex((kindleDatabase[1])['UsernameHash']) print("Got UsernameHash from database {0}".format(kindleDatabase[0])) except KeyError: # Get the UserName we added UserName = bytearray.fromhex((kindleDatabase[1])['UserName']) # encode it encodedUsername = encodeHash(UserName,charMap1) #print "encodedUsername",encodedUsername.encode('hex') except KeyError: print("Keys not found in the database {0}.".format(kindleDatabase[0])) return pids # Get the ID string used encodedIDString = encodeHash(IDString,charMap1) #print "encodedIDString",encodedIDString.encode('hex') # concat, hash and encode to calculate the DSN DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1) #print "DSN",DSN.encode('hex') pass if rec209 is None: pids.append(DSN+kindleAccountToken) return pids # Compute the device PID (for which I can tell, is used for nothing). table = generatePidEncryptionTable() devicePID = generateDevicePID(table,DSN,4) devicePID = checksumPid(devicePID) pids.append(devicePID) # Compute book PIDs # book pid pidHash = SHA1(DSN+kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) pids.append(bookPID) # variant 1 pidHash = SHA1(kindleAccountToken+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) pids.append(bookPID) # variant 2 pidHash = SHA1(DSN+rec209+token) bookPID = encodePID(pidHash) bookPID = checksumPid(bookPID) pids.append(bookPID) return pids def getPidList(md1, md2, serials=[], kDatabases=[]): pidlst = [] if kDatabases is None: kDatabases = [] if serials is None: serials = [] for kDatabase in kDatabases: try: pidlst.extend(map(bytes,getK4Pids(md1, md2, kDatabase))) except Exception as e: print("Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0])) traceback.print_exc() for serialnum in serials: try: pidlst.extend(map(bytes,getKindlePids(md1, md2, serialnum))) except Exception as e: print("Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])) traceback.print_exc() return pidlst ================================================ FILE: DeDRM_plugin/kindlekey.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # kindlekey.py # Copyright © 2008-2020 Apprentice Harper et al. __license__ = 'GPL v3' __version__ = '3.0' # Revision history: # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 1.1 - Added Tkinter to match adobekey.py # 1.2 - Fixed testing of successful retrieval on Mac # 1.3 - Added getkey interface for Windows DeDRM application # Simplified some of the Kindle for Mac code. # 1.4 - Remove dependency on alfcrypto # 1.5 - moved unicode_argv call inside main for Windows DeDRM compatibility # 1.6 - Fixed a problem getting the disk serial numbers # 1.7 - Work if TkInter is missing # 1.8 - Fixes for Kindle for Mac, and non-ascii in Windows user names # 1.9 - Fixes for Unicode in Windows user names # 2.0 - Added comments and extra fix for non-ascii Windows user names # 2.1 - Fixed Kindle for PC encryption changes March 2016 # 2.2 - Fixes for Macs with bonded ethernet ports # Also removed old .kinfo file support (pre-2011) # 2.3 - Added more field names thanks to concavegit's KFX code. # 2.4 - Fix for complex Mac disk setups, thanks to Tibs # 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus # 2.6 - Start adding support for Kindle 1.25+ .kinf2018 file # 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya # 2.8 - Fix for Mac OS X Big Sur # 3.0 - Python 3 for calibre 5.0 """ Retrieve Kindle for PC/Mac user key. """ import sys, os, re import codecs from struct import pack, unpack, unpack_from import json import getopt import traceback try: RegError except NameError: class RegError(Exception): pass # Routines common to Mac and PC # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) try: from calibre.constants import iswindows, isosx except: iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv # as a list of Unicode strings and encode them as utf-8 from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["kindlekey.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class DrmException(Exception): pass # crypto digestroutines import hashlib def MD5(message): ctx = hashlib.md5() ctx.update(message) return ctx.digest() def SHA1(message): ctx = hashlib.sha1() ctx.update(message) return ctx.digest() def SHA256(message): ctx = hashlib.sha256() ctx.update(message) return ctx.digest() # For K4M/PC 1.6.X and later def primes(n): """ Return a list of prime integers smaller than or equal to n :param n: int :return: list->int """ if n == 2: return [2] elif n < 2: return [] primeList = [2] for potentialPrime in range(3, n + 1, 2): isItPrime = True for prime in primeList: if potentialPrime % prime == 0: isItPrime = False if isItPrime is True: primeList.append(potentialPrime) return primeList # Encode the bytes in data with the characters in map # data and map should be byte arrays def encode(data, map): result = b'' for char in data: value = char Q = (value ^ 0x80) // len(map) R = value % len(map) result += bytes([map[Q]]) result += bytes([map[R]]) return result # Hash the bytes in data and then encode the digest with the characters in map def encodeHash(data,map): return encode(MD5(data),map) # Decode the string in data with the characters in map. Returns the decoded bytes def decode(data,map): result = b'' for i in range (0,len(data)-1,2): high = map.find(data[i]) low = map.find(data[i+1]) if (high == -1) or (low == -1) : break value = (((high * len(map)) ^ 0x80) & 0xFF) + low result += pack('B',value) return result # Routines unique to Mac and PC if iswindows: from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ string_at, Structure, c_void_p, cast import winreg MAX_PATH = 255 kernel32 = windll.kernel32 advapi32 = windll.advapi32 crypt32 = windll.crypt32 try: # try to get fast routines from alfcrypto from alfcrypto import AES_CBC, KeyIVGen except: # alfcrypto not available, so use python implementations """ Routines for doing AES CBC in one file Modified by some_updates to extract and combine only those parts needed for AES CBC into one simple to add python file Original Version Copyright (c) 2002 by Paul A. Lambert Under: CryptoPy Artistic License Version 1.0 See the wonderful pure python package cryptopy-1.2.5 and read its LICENSE.txt for complete license details. """ class CryptoError(Exception): """ Base class for crypto exceptions """ def __init__(self,errorMessage='Error!'): self.message = errorMessage def __str__(self): return self.message class InitCryptoError(CryptoError): """ Crypto errors during algorithm initialization """ class BadKeySizeError(InitCryptoError): """ Bad key size error """ class EncryptError(CryptoError): """ Error in encryption processing """ class DecryptError(CryptoError): """ Error in decryption processing """ class DecryptNotBlockAlignedError(DecryptError): """ Error in decryption processing """ def xor(a,b): """ XOR two byte arrays, to lesser length """ x = [] for i in range(min(len(a),len(b))): x.append( a[i] ^ b[i]) return bytes(x) """ Base 'BlockCipher' and Pad classes for cipher instances. BlockCipher supports automatic padding and type conversion. The BlockCipher class was written to make the actual algorithm code more readable and not for performance. """ class BlockCipher: """ Block ciphers """ def __init__(self): self.reset() def reset(self): self.resetEncrypt() self.resetDecrypt() def resetEncrypt(self): self.encryptBlockCount = 0 self.bytesToEncrypt = b'' def resetDecrypt(self): self.decryptBlockCount = 0 self.bytesToDecrypt = b'' def encrypt(self, plainText, more = None): """ Encrypt a string and return a binary string """ self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize) cipherText = '' for i in range(numBlocks): bStart = i*self.blockSize ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize]) self.encryptBlockCount += 1 cipherText += ctBlock if numExtraBytes > 0: # save any bytes that are not block aligned self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] else: self.bytesToEncrypt = '' if more == None: # no more data expected from caller finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize) if len(finalBytes) > 0: ctBlock = self.encryptBlock(finalBytes) self.encryptBlockCount += 1 cipherText += ctBlock self.resetEncrypt() return cipherText def decrypt(self, cipherText, more = None): """ Decrypt a string and return a string """ self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) if more == None: # no more calls to decrypt, should have all the data if numExtraBytes != 0: raise DecryptNotBlockAlignedError('Data not block aligned on decrypt') # hold back some bytes in case last decrypt has zero len if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : numBlocks -= 1 numExtraBytes = self.blockSize plainText = b'' for i in range(numBlocks): bStart = i*self.blockSize ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) self.decryptBlockCount += 1 plainText += ptBlock if numExtraBytes > 0: # save any bytes that are not block aligned self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:] else: self.bytesToEncrypt = '' if more == None: # last decrypt remove padding plainText = self.padding.removePad(plainText, self.blockSize) self.resetDecrypt() return plainText class Pad: def __init__(self): pass # eventually could put in calculation of min and max size extension class padWithPadLen(Pad): """ Pad a binary string with the length of the padding """ def addPad(self, extraBytes, blockSize): """ Add padding to a binary string to make it an even multiple of the block size """ blocks, numExtraBytes = divmod(len(extraBytes), blockSize) padLength = blockSize - numExtraBytes return extraBytes + padLength*chr(padLength) def removePad(self, paddedBinaryString, blockSize): """ Remove padding from a binary string """ if not(0 6 and i%Nk == 4 : temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) return w Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!! 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6, 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91) #------------------------------------- def AddRoundKey(algInstance, keyBlock): """ XOR the algorithm state with a block of key material """ for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] ^= keyBlock[column][row] #------------------------------------- def SubBytes(algInstance): for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] = Sbox[algInstance.state[column][row]] def InvSubBytes(algInstance): for column in range(algInstance.Nb): for row in range(4): algInstance.state[column][row] = InvSbox[algInstance.state[column][row]] Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5, 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0, 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc, 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a, 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0, 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b, 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85, 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5, 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17, 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88, 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c, 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9, 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6, 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e, 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94, 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68, 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16) InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38, 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87, 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d, 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2, 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16, 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda, 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a, 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02, 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea, 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85, 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89, 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20, 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31, 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d, 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0, 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26, 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d) #------------------------------------- """ For each block size (Nb), the ShiftRow operation shifts row i by the amount Ci. Note that row 0 is not shifted. Nb C1 C2 C3 ------------------- """ shiftOffset = { 4 : ( 0, 1, 2, 3), 5 : ( 0, 1, 2, 3), 6 : ( 0, 1, 2, 3), 7 : ( 0, 1, 2, 4), 8 : ( 0, 1, 3, 4) } def ShiftRows(algInstance): tmp = [0]*algInstance.Nb # list of size Nb for r in range(1,4): # row 0 reamains unchanged and can be skipped for c in range(algInstance.Nb): tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] for c in range(algInstance.Nb): algInstance.state[c][r] = tmp[c] def InvShiftRows(algInstance): tmp = [0]*algInstance.Nb # list of size Nb for r in range(1,4): # row 0 reamains unchanged and can be skipped for c in range(algInstance.Nb): tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r] for c in range(algInstance.Nb): algInstance.state[c][r] = tmp[c] #------------------------------------- def MixColumns(a): Sprime = [0,0,0,0] for j in range(a.Nb): # for each column Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3]) Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3]) Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3]) Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3]) for i in range(4): a.state[j][i] = Sprime[i] def InvMixColumns(a): """ Mix the four bytes of every column in a linear way This is the opposite operation of Mixcolumn """ Sprime = [0,0,0,0] for j in range(a.Nb): # for each column Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3]) Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3]) Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3]) Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3]) for i in range(4): a.state[j][i] = Sprime[i] #------------------------------------- def mul(a, b): """ Multiply two elements of GF(2^m) needed for MixColumn and InvMixColumn """ if (a !=0 and b!=0): return Alogtable[(Logtable[a] + Logtable[b])%255] else: return 0 Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3, 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193, 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120, 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142, 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56, 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16, 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186, 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87, 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232, 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160, 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7) Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53, 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170, 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49, 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205, 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136, 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154, 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163, 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160, 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65, 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23, 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1) """ AES Encryption Algorithm The AES algorithm is just Rijndael algorithm restricted to the default blockSize of 128 bits. """ class AES(Rijndael): """ The AES algorithm is the Rijndael block cipher restricted to block sizes of 128 bits and key sizes of 128, 192 or 256 bits """ def __init__(self, key = None, padding = padWithPadLen(), keySize=16): """ Initialize AES, keySize is in bytes """ if not (keySize == 16 or keySize == 24 or keySize == 32) : raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes') Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) self.name = 'AES' """ CBC mode of encryption for block ciphers. This algorithm mode wraps any BlockCipher to make a Cipher Block Chaining mode. """ from random import Random # should change to crypto.random!!! class CBC(BlockCipher): """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode algorithms. The initialization (IV) is automatic if set to None. Padding is also automatic based on the Pad class used to initialize the algorithm """ def __init__(self, blockCipherInstance, padding = padWithPadLen()): """ CBC algorithms are created by initializing with a BlockCipher instance """ self.baseCipher = blockCipherInstance self.name = self.baseCipher.name + '_CBC' self.blockSize = self.baseCipher.blockSize self.keySize = self.baseCipher.keySize self.padding = padding self.baseCipher.padding = noPadding() # baseCipher should NOT pad!! self.r = Random() # for IV generation, currently uses # mediocre standard distro version <---------------- import time newSeed = time.ctime()+str(self.r) # seed with instance location self.r.seed(newSeed) # to make unique self.reset() def setKey(self, key): self.baseCipher.setKey(key) # Overload to reset both CBC state and the wrapped baseCipher def resetEncrypt(self): BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class) self.baseCipher.resetEncrypt() # reset base cipher encrypt state def resetDecrypt(self): BlockCipher.resetDecrypt(self) # reset CBC state (super class) self.baseCipher.resetDecrypt() # reset base cipher decrypt state def encrypt(self, plainText, iv=None, more=None): """ CBC encryption - overloads baseCipher to allow optional explicit IV when iv=None, iv is auto generated! """ if self.encryptBlockCount == 0: self.iv = iv else: assert(iv==None), 'IV used only on first call to encrypt' return BlockCipher.encrypt(self,plainText, more=more) def decrypt(self, cipherText, iv=None, more=None): """ CBC decryption - overloads baseCipher to allow optional explicit IV when iv=None, iv is auto generated! """ if self.decryptBlockCount == 0: self.iv = iv else: assert(iv==None), 'IV used only on first call to decrypt' return BlockCipher.decrypt(self, cipherText, more=more) def encryptBlock(self, plainTextBlock): """ CBC block encryption, IV is set with 'encrypt' """ auto_IV = '' if self.encryptBlockCount == 0: if self.iv == None: # generate IV and use self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)]) self.prior_encr_CT_block = self.iv auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic else: # application provided IV assert(len(self.iv) == self.blockSize ),'IV must be same length as block' self.prior_encr_CT_block = self.iv """ encrypt the prior CT XORed with the PT """ ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) ) self.prior_encr_CT_block = ct return auto_IV+ct def decryptBlock(self, encryptedBlock): """ Decrypt a single block """ if self.decryptBlockCount == 0: # first call, process IV if self.iv == None: # auto decrypt IV? self.prior_CT_block = encryptedBlock return b'' else: assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" self.prior_CT_block = self.iv dct = self.baseCipher.decryptBlock(encryptedBlock) """ XOR the prior decrypted CT with the prior CT """ dct_XOR_priorCT = xor( self.prior_CT_block, dct ) self.prior_CT_block = encryptedBlock return dct_XOR_priorCT """ AES_CBC Encryption Algorithm """ class aescbc_AES_CBC(CBC): """ AES encryption in CBC feedback mode """ def __init__(self, key=None, padding=padWithPadLen(), keySize=16): CBC.__init__( self, AES(key, noPadding(), keySize), padding) self.name = 'AES_CBC' class AES_CBC(object): def __init__(self): self._key = None self._iv = None self.aes = None def set_decrypt_key(self, userkey, iv): self._key = userkey self._iv = iv self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey)) def decrypt(self, data): iv = self._iv cleartext = self.aes.decrypt(iv + data) return cleartext import hmac class KeyIVGen(object): # this only exists in openssl so we will use pure python implementation instead # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) def pbkdf2(self, passwd, salt, iter, keylen): def xorbytes( a, b ): if len(a) != len(b): raise Exception("xorbytes(): lengths differ") return bytes([x ^ y for x, y in zip(a, b)]) def prf( h, data ): hm = h.copy() hm.update( data ) return hm.digest() def pbkdf2_F( h, salt, itercount, blocknum ): U = prf( h, salt + pack('>i',blocknum ) ) T = U for i in range(2, itercount+1): U = prf( h, U ) T = xorbytes( T, U ) return T sha = hashlib.sha1 digest_size = sha().digest_size # l - number of output blocks to produce l = keylen // digest_size if keylen % digest_size != 0: l += 1 h = hmac.new( passwd, None, sha ) T = b"" for i in range(1, l+1): T += pbkdf2_F( h, salt, iter, i ) return T[0: keylen] def UnprotectHeaderData(encryptedData): passwdData = b'header_key_data' salt = b'HEADER.2011' iter = 0x80 keylen = 0x100 key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] aes=AES_CBC() aes.set_decrypt_key(key, iv) cleartext = aes.decrypt(encryptedData) return cleartext # Various character maps used to decrypt kindle info values. # Probably supposed to act as obfuscation charMap2 = b"AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" charMap5 = b"AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" # New maps in K4PC 1.9.0 testMap1 = b"n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" testMap6 = b"9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" testMap8 = b"YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" # interface with Windows OS Routines class DataBlob(Structure): _fields_ = [('cbData', c_uint), ('pbData', c_void_p)] DataBlob_p = POINTER(DataBlob) def GetSystemDirectory(): GetSystemDirectoryW = kernel32.GetSystemDirectoryW GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint] GetSystemDirectoryW.restype = c_uint def GetSystemDirectory(): buffer = create_unicode_buffer(MAX_PATH + 1) GetSystemDirectoryW(buffer, len(buffer)) return buffer.value return GetSystemDirectory GetSystemDirectory = GetSystemDirectory() def GetVolumeSerialNumber(): GetVolumeInformationW = kernel32.GetVolumeInformationW GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint, POINTER(c_uint), POINTER(c_uint), POINTER(c_uint), c_wchar_p, c_uint] GetVolumeInformationW.restype = c_uint def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'): vsn = c_uint(0) GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0) return str(vsn.value) return GetVolumeSerialNumber GetVolumeSerialNumber = GetVolumeSerialNumber() def GetIDString(): vsn = GetVolumeSerialNumber() #print('Using Volume Serial Number for ID: '+vsn) return vsn def getLastError(): GetLastError = kernel32.GetLastError GetLastError.argtypes = None GetLastError.restype = c_uint def getLastError(): return GetLastError() return getLastError getLastError = getLastError() def GetUserName(): GetUserNameW = advapi32.GetUserNameW GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)] GetUserNameW.restype = c_uint def GetUserName(): buffer = create_unicode_buffer(2) size = c_uint(len(buffer)) while not GetUserNameW(buffer, byref(size)): errcd = getLastError() if errcd == 234: # bad wine implementation up through wine 1.3.21 return "AlternateUserName" # double the buffer size buffer = create_unicode_buffer(len(buffer) * 2) size.value = len(buffer) # replace any non-ASCII values with 0xfffd for i in range(0,len(buffer)): if buffer[i]>"\u007f": #print "swapping char "+str(i)+" ("+buffer[i]+")" buffer[i] = "\ufffd" # return utf-8 encoding of modified username #print "modified username:"+buffer.value return buffer.value.encode('utf-8') return GetUserName GetUserName = GetUserName() def CryptUnprotectData(): _CryptUnprotectData = crypt32.CryptUnprotectData _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p, c_void_p, c_void_p, c_uint, DataBlob_p] _CryptUnprotectData.restype = c_uint def CryptUnprotectData(indata, entropy, flags): indatab = create_string_buffer(indata) indata = DataBlob(len(indata), cast(indatab, c_void_p)) entropyb = create_string_buffer(entropy) entropy = DataBlob(len(entropy), cast(entropyb, c_void_p)) outdata = DataBlob() if not _CryptUnprotectData(byref(indata), None, byref(entropy), None, None, flags, byref(outdata)): # raise DrmException("Failed to Unprotect Data") return b'failed' return string_at(outdata.pbData, outdata.cbData) return CryptUnprotectData CryptUnprotectData = CryptUnprotectData() # Returns Environmental Variables that contain unicode # name must be unicode string, not byte string. def getEnvironmentVariable(name): import ctypes n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) if n == 0: return None buf = ctypes.create_unicode_buffer("\0"*n) ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) return buf.value # Locate all of the kindle-info style files and return as list def getKindleInfoFiles(): kInfoFiles = [] # some 64 bit machines do not have the proper registry key for some reason # or the python interface to the 32 vs 64 bit registry is broken path = "" if 'LOCALAPPDATA' in os.environ.keys(): # Python 2.x does not return unicode env. Use Python 3.x path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%") # this is just another alternative. # path = getEnvironmentVariable('LOCALAPPDATA') if not os.path.isdir(path): path = "" else: # User Shell Folders show take precedent over Shell Folders if present try: # this will still break regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if not os.path.isdir(path): path = "" try: regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] if not os.path.isdir(path): path = "" except RegError: pass except RegError: pass found = False if path == "": print ('Could not find the folder in which to look for kinfoFiles.') else: # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 print("searching for kinfoFiles in " + path) # look for (K4PC 1.25.1 and later) .kinf2018 file kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2018' if os.path.isfile(kinfopath): found = True print('Found K4PC 1.25+ kinf2018 file: ' + kinfopath) kInfoFiles.append(kinfopath) # look for (K4PC 1.9.0 and later) .kinf2011 file kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' if os.path.isfile(kinfopath): found = True print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath) kInfoFiles.append(kinfopath) # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf' if os.path.isfile(kinfopath): found = True print('Found K4PC 1.6-1.8 kinf file: ' + kinfopath) kInfoFiles.append(kinfopath) # look for (K4PC 1.5.0 and later) rainier.2.1.1.kinf file kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf' if os.path.isfile(kinfopath): found = True print('Found K4PC 1.5 kinf file: ' + kinfopath) kInfoFiles.append(kinfopath) # look for original (earlier than K4PC 1.5.0) kindle-info files kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' if os.path.isfile(kinfopath): found = True print('Found K4PC kindle.info file: ' + kinfopath) kInfoFiles.append(kinfopath) if not found: print('No K4PC kindle.info/kinf/kinf2011 files have been found.') return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): names = [\ b'kindle.account.tokens',\ b'kindle.cookie.item',\ b'eulaVersionAccepted',\ b'login_date',\ b'kindle.token.item',\ b'login',\ b'kindle.key.item',\ b'kindle.name.info',\ b'kindle.device.info',\ b'MazamaRandomNumber',\ b'max_date',\ b'SIGVERIF',\ b'build_version',\ b'SerialNumber',\ b'UsernameHash',\ b'kindle.directedid.info',\ b'DSN',\ b'kindle.accounttype.info',\ b'krx.flashcardsplugin.data.encryption_key',\ b'krx.notebookexportplugin.data.encryption_key',\ b'proxy.http.password',\ b'proxy.http.username' ] namehashmap = {encodeHash(n,testMap8):n for n in names} # print(namehashmap) DB = {} with open(kInfoFile, 'rb') as infoReader: data = infoReader.read() # assume .kinf2011 or .kinf2018 style .kinf file # the .kinf file uses "/" to separate it into records # so remove the trailing "/" to make it easy to use split data = data[:-1] items = data.split(b'/') # starts with an encoded and encrypted header blob headerblob = items.pop(0) encryptedValue = decode(headerblob, testMap1) cleartext = UnprotectHeaderData(encryptedValue) #print "header cleartext:",cleartext # now extract the pieces that form the added entropy pattern = re.compile(br'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) for m in re.finditer(pattern, cleartext): version = int(m.group(1)) build = m.group(2) guid = m.group(4) if version == 5: # .kinf2011 added_entropy = build + guid elif version == 6: # .kinf2018 salt = str(0x6d8 * int(build)).encode('utf-8') + guid sp = GetUserName() + b'+@#$%+' + GetIDString().encode('utf-8') passwd = encode(SHA256(sp), charMap5) key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow # loop through the item records until all are processed while len(items) > 0: # get the first item record item = items.pop(0) # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] # the remainder of the first record when decoded with charMap5 # has the ':' split char followed by the string representation # of the number of records that follow # and make up the contents srcnt = decode(item[34:],charMap5) rcnt = int(srcnt) # read and store in rcnt records of data # that make up the contents value edlst = [] for i in range(rcnt): item = items.pop(0) edlst.append(item) # key names now use the new testMap8 encoding if keyhash in namehashmap: keyname=namehashmap[keyhash] #print "keyname found from hash:",keyname else: keyname = keyhash #print "keyname not found, hash is:",keyname # the testMap8 encoded contents data has had a length # of chars (always odd) cut off of the front and moved # to the end to prevent decoding using testMap8 from # working properly, and thereby preventing the ensuing # CryptUnprotectData call from succeeding. # The offset into the testMap8 encoded contents seems to be: # len(contents)-largest prime number <= int(len(content)/3) # (in other words split "about" 2/3rds of the way through) # move first offsets chars to end to align for decode by testMap8 # by moving noffset chars from the start of the # string to the end of the string encdata = b"".join(edlst) #print "encrypted data:",encdata contlen = len(encdata) noffset = contlen - primes(int(contlen/3))[-1] pfx = encdata[0:noffset] encdata = encdata[noffset:] encdata = encdata + pfx #print "rearranged data:",encdata if version == 5: # decode using new testMap8 to get the original CryptProtect Data encryptedValue = decode(encdata,testMap8) #print "decoded data:",encryptedValue.encode('hex') entropy = SHA1(keyhash) + added_entropy cleartext = CryptUnprotectData(encryptedValue, entropy, 1) elif version == 6: from Crypto.Cipher import AES from Crypto.Util import Counter # decode using new testMap8 to get IV + ciphertext iv_ciphertext = decode(encdata, testMap8) # pad IV so that we can substitute AES-CTR for GCM iv = iv_ciphertext[:12] + b'\x00\x00\x00\x02' ciphertext = iv_ciphertext[12:] # convert IV to int for use with pycrypto iv_ints = unpack('>QQ', iv) iv = iv_ints[0] << 64 | iv_ints[1] # set up AES-CTR ctr = Counter.new(128, initial_value=iv) cipher = AES.new(key, AES.MODE_CTR, counter=ctr) # decrypt and decode cleartext = decode(cipher.decrypt(ciphertext), charMap5) if len(cleartext)>0: #print "cleartext data:",cleartext,":end data" DB[keyname] = cleartext #print keyname, cleartext if len(DB)>6: # store values used in decryption DB[b'IDString'] = GetIDString().encode('utf-8') DB[b'UserName'] = GetUserName() print("Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode('utf-8'))) else: print("Couldn't decrypt file.") DB = {} return DB elif isosx: import copy import subprocess # interface to needed routines in openssl's libcrypto def _load_crypto_libcrypto(): from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, addressof, string_at, cast from ctypes.util import find_library libcrypto = find_library('crypto') if libcrypto is None: libcrypto = '/usr/lib/libcrypto.dylib' try: libcrypto = CDLL(libcrypto) except Exception as e: raise DrmException("libcrypto not found: " % e) # From OpenSSL's crypto aes header # # AES_ENCRYPT 1 # AES_DECRYPT 0 # AES_MAXNR 14 (in bytes) # AES_BLOCK_SIZE 16 (in bytes) # # struct aes_key_st { # unsigned long rd_key[4 *(AES_MAXNR + 1)]; # int rounds; # }; # typedef struct aes_key_st AES_KEY; # # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key); # # note: the ivec string, and output buffer are both mutable # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc); AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func 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]) # From OpenSSL's Crypto evp/p5_crpt2.c # # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, # const unsigned char *salt, int saltlen, int iter, # int keylen, unsigned char *out); PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) class LibCrypto(object): def __init__(self): self._blocksize = 0 self._keyctx = None self._iv = 0 def set_decrypt_key(self, userkey, iv): self._blocksize = len(userkey) if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : raise DrmException("AES improper key used") return keyctx = self._keyctx = AES_KEY() self._iv = iv self._userkey = userkey rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) if rv < 0: raise DrmException("Failed to initialize AES key") def decrypt(self, data): out = create_string_buffer(len(data)) mutable_iv = create_string_buffer(self._iv, len(self._iv)) keyctx = self._keyctx rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) if rv == 0: raise DrmException("AES decryption failed") return out.raw def keyivgen(self, passwd, salt, iter, keylen): saltlen = len(salt) passlen = len(passwd) out = create_string_buffer(keylen) rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out) return out.raw return LibCrypto def _load_crypto(): LibCrypto = None try: LibCrypto = _load_crypto_libcrypto() except (ImportError, DrmException): pass return LibCrypto LibCrypto = _load_crypto() # Various character maps used to decrypt books. Probably supposed to act as obfuscation charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' charMap2 = b'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' # For kinf approach of K4Mac 1.6.X or later # On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' # For Mac they seem to re-use charMap2 here charMap5 = charMap2 # new in K4M 1.9.X testMap8 = b'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' # uses a sub process to get the Hard Drive Serial Number using ioreg # returns serial numbers of all internal hard drive drives def GetVolumesSerialNumbers(): sernums = [] sernum = os.getenv('MYSERIALNUMBER') if sernum != None: sernums.append(sernum.strip()) cmdline = '/usr/sbin/ioreg -w 0 -r -c AppleAHCIDiskDriver' cmdline = cmdline.encode(sys.getfilesystemencoding()) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) out1, out2 = p.communicate() #print out1 reslst = out1.split(b'\n') cnt = len(reslst) for j in range(cnt): resline = reslst[j] pp = resline.find(b'\"Serial Number\" = \"') if pp >= 0: sernum = resline[pp+19:-1] sernums.append(sernum.strip()) return sernums def GetDiskPartitionNames(): names = [] cmdline = '/sbin/mount' cmdline = cmdline.encode(sys.getfilesystemencoding()) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) out1, out2 = p.communicate() reslst = out1.split(b'\n') cnt = len(reslst) for j in range(cnt): resline = reslst[j] if resline.startswith(b'/dev'): (devpart, mpath) = resline.split(b' on ')[:2] dpart = devpart[5:] names.append(dpart) return names # uses a sub process to get the UUID of all disk partitions def GetDiskPartitionUUIDs(): uuids = [] uuidnum = os.getenv('MYUUIDNUMBER') if uuidnum != None: uuids.append(uuidnum.strip()) cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver' cmdline = cmdline.encode(sys.getfilesystemencoding()) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) out1, out2 = p.communicate() #print out1 reslst = out1.split(b'\n') cnt = len(reslst) for j in range(cnt): resline = reslst[j] pp = resline.find(b'\"UUID\" = \"') if pp >= 0: uuidnum = resline[pp+10:-1] uuidnum = uuidnum.strip() uuids.append(uuidnum) return uuids def GetMACAddressesMunged(): macnums = [] macnum = os.getenv('MYMACNUM') if macnum != None: macnums.append(macnum) cmdline = 'networksetup -listallhardwareports' # en0' cmdline = cmdline.encode(sys.getfilesystemencoding()) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) out1, out2 = p.communicate() reslst = out1.split(b'\n') cnt = len(reslst) for j in range(cnt): resline = reslst[j] pp = resline.find(b'Ethernet Address: ') if pp >= 0: #print resline macnum = resline[pp+18:] macnum = macnum.strip() maclst = macnum.split(b':') n = len(maclst) if n != 6: continue #print 'original mac', macnum # now munge it up the way Kindle app does # by xoring it with 0xa5 and swapping elements 3 and 4 for i in range(6): maclst[i] = int(b'0x' + maclst[i], 0) mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst[5] = maclst[5] ^ 0xa5 mlst[4] = maclst[3] ^ 0xa5 mlst[3] = maclst[4] ^ 0xa5 mlst[2] = maclst[2] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5 mlst[0] = maclst[0] ^ 0xa5 macnum = b'%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) #print 'munged mac', macnum macnums.append(macnum) return macnums # uses unix env to get username instead of using sysctlbyname def GetUserName(): username = os.getenv('USER') #print "Username:",username return username.encode('utf-8') def GetIDStrings(): # Return all possible ID Strings strings = [] strings.extend(GetMACAddressesMunged()) strings.extend(GetVolumesSerialNumbers()) strings.extend(GetDiskPartitionNames()) strings.extend(GetDiskPartitionUUIDs()) strings.append(b'9999999999') #print "ID Strings:\n",strings return strings # unprotect the new header blob in .kinf2011 # used in Kindle for Mac Version >= 1.9.0 def UnprotectHeaderData(encryptedData): passwdData = b'header_key_data' salt = b'HEADER.2011' iter = 0x80 keylen = 0x100 crp = LibCrypto() key_iv = crp.keyivgen(passwdData, salt, iter, keylen) key = key_iv[0:32] iv = key_iv[32:48] crp.set_decrypt_key(key,iv) cleartext = crp.decrypt(encryptedData) return cleartext # implements an Pseudo Mac Version of Windows built-in Crypto routine class CryptUnprotectData(object): def __init__(self, entropy, IDString): sp = GetUserName() + b'+@#$%+' + IDString passwdData = encode(SHA256(sp),charMap2) salt = entropy self.crp = LibCrypto() iter = 0x800 keylen = 0x400 key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen) self.key = key_iv[0:32] self.iv = key_iv[32:48] self.crp.set_decrypt_key(self.key, self.iv) def decrypt(self, encryptedData): cleartext = self.crp.decrypt(encryptedData) cleartext = decode(cleartext, charMap2) return cleartext # Locate the .kindle-info files def getKindleInfoFiles(): # file searches can take a long time on some systems, so just look in known specific places. kInfoFiles=[] found = False home = os.getenv('HOME') # check for .kinf2018 file in new location (App Store Kindle for Mac) testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2018' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kinf2018 file: ' + testpath) found = True # check for .kinf2018 files testpath = home + '/Library/Application Support/Kindle/storage/.kinf2018' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kinf2018 file: ' + testpath) found = True # check for .kinf2011 file in new location (App Store Kindle for Mac) testpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.kinf2011' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kinf2011 file: ' + testpath) found = True # check for .kinf2011 files from 1.10 testpath = home + '/Library/Application Support/Kindle/storage/.kinf2011' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kinf2011 file: ' + testpath) found = True # check for .rainier-2.1.1-kinf files from 1.6 testpath = home + '/Library/Application Support/Kindle/storage/.rainier-2.1.1-kinf' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac rainier file: ' + testpath) found = True # check for .kindle-info files from 1.4 testpath = home + '/Library/Application Support/Kindle/storage/.kindle-info' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kindle-info file: ' + testpath) found = True # check for .kindle-info file from 1.2.2 testpath = home + '/Library/Application Support/Amazon/Kindle/storage/.kindle-info' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kindle-info file: ' + testpath) found = True # check for .kindle-info file from 1.0 beta 1 (27214) testpath = home + '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info' if os.path.isfile(testpath): kInfoFiles.append(testpath) print('Found k4Mac kindle-info file: ' + testpath) found = True if not found: print('No k4Mac kindle-info/rainier/kinf2011 files have been found.') return kInfoFiles # determine type of kindle info provided and return a # database of keynames and values def getDBfromFile(kInfoFile): names = [\ b'kindle.account.tokens',\ b'kindle.cookie.item',\ b'eulaVersionAccepted',\ b'login_date',\ b'kindle.token.item',\ b'login',\ b'kindle.key.item',\ b'kindle.name.info',\ b'kindle.device.info',\ b'MazamaRandomNumber',\ b'max_date',\ b'SIGVERIF',\ b'build_version',\ b'SerialNumber',\ b'UsernameHash',\ b'kindle.directedid.info',\ b'DSN' b'kindle.accounttype.info',\ b'krx.flashcardsplugin.data.encryption_key',\ b'krx.notebookexportplugin.data.encryption_key',\ b'proxy.http.password',\ b'proxy.http.username' ] with open(kInfoFile, 'rb') as infoReader: filedata = infoReader.read() data = filedata[:-1] items = data.split(b'/') IDStrings = GetIDStrings() print ("trying username ", GetUserName(), " on file ", kInfoFile) for IDString in IDStrings: print ("trying IDString:",IDString) try: DB = {} items = data.split(b'/') # the headerblob is the encrypted information needed to build the entropy string headerblob = items.pop(0) #print ("headerblob: ",headerblob) encryptedValue = decode(headerblob, charMap1) #print ("encryptedvalue: ",encryptedValue) cleartext = UnprotectHeaderData(encryptedValue) #print ("cleartext: ",cleartext) # now extract the pieces in the same way pattern = re.compile(br'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) for m in re.finditer(pattern, cleartext): version = int(m.group(1)) build = m.group(2) guid = m.group(4) #print ("version",version) #print ("build",build) #print ("guid",guid,"\n") if version == 5: # .kinf2011: identical to K4PC, except the build number gets multiplied entropy = str(0x2df * int(build)).encode('utf-8') + guid cud = CryptUnprotectData(entropy,IDString) #print ("entropy",entropy) #print ("cud",cud) elif version == 6: # .kinf2018: identical to K4PC salt = str(0x6d8 * int(build)).encode('utf-8') + guid sp = GetUserName() + b'+@#$%+' + IDString passwd = encode(SHA256(sp), charMap5) key = LibCrypto().keyivgen(passwd, salt, 10000, 0x400)[:32] #print ("salt",salt) #print ("sp",sp) #print ("passwd",passwd) #print ("key",key) # loop through the item records until all are processed while len(items) > 0: # get the first item record item = items.pop(0) # the first 32 chars of the first record of a group # is the MD5 hash of the key name encoded by charMap5 keyhash = item[0:32] keyname = b'unknown' # unlike K4PC the keyhash is not used in generating entropy # entropy = SHA1(keyhash) + added_entropy # entropy = added_entropy # the remainder of the first record when decoded with charMap5 # has the ':' split char followed by the string representation # of the number of records that follow # and make up the contents srcnt = decode(item[34:],charMap5) rcnt = int(srcnt) # read and store in rcnt records of data # that make up the contents value edlst = [] for i in range(rcnt): item = items.pop(0) edlst.append(item) keyname = b'unknown' for name in names: if encodeHash(name,testMap8) == keyhash: keyname = name break if keyname == b'unknown': keyname = keyhash # the testMap8 encoded contents data has had a length # of chars (always odd) cut off of the front and moved # to the end to prevent decoding using testMap8 from # working properly, and thereby preventing the ensuing # CryptUnprotectData call from succeeding. # The offset into the testMap8 encoded contents seems to be: # len(contents) - largest prime number less than or equal to int(len(content)/3) # (in other words split 'about' 2/3rds of the way through) # move first offsets chars to end to align for decode by testMap8 encdata = b''.join(edlst) contlen = len(encdata) # now properly split and recombine # by moving noffset chars from the start of the # string to the end of the string noffset = contlen - primes(int(contlen/3))[-1] pfx = encdata[0:noffset] encdata = encdata[noffset:] encdata = encdata + pfx if version == 5: # decode using testMap8 to get the CryptProtect Data encryptedValue = decode(encdata,testMap8) cleartext = cud.decrypt(encryptedValue) elif version == 6: from Crypto.Cipher import AES from Crypto.Util import Counter # decode using new testMap8 to get IV + ciphertext iv_ciphertext = decode(encdata, testMap8) # pad IV so that we can substitute AES-CTR for GCM iv = iv_ciphertext[:12] + b'\x00\x00\x00\x02' ciphertext = iv_ciphertext[12:] # convert IV to int for use with pycrypto iv_ints = unpack('>QQ', iv) iv = iv_ints[0] << 64 | iv_ints[1] # set up AES-CTR ctr = Counter.new(128, initial_value=iv) cipher = AES.new(key, AES.MODE_CTR, counter=ctr) # decrypt and decode cleartext = decode(cipher.decrypt(ciphertext), charMap5) # print keyname # print cleartext if len(cleartext) > 0: DB[keyname] = cleartext if len(DB)>6: break except Exception: print (traceback.format_exc()) pass if len(DB)>6: # store values used in decryption print("Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString.decode('utf-8'), GetUserName().decode('utf-8'))) DB[b'IDString'] = IDString DB[b'UserName'] = GetUserName() else: print("Couldn't decrypt file.") DB = {} return DB else: def getDBfromFile(kInfoFile): raise DrmException("This script only runs under Windows or Mac OS X.") return {} def kindlekeys(files = []): keys = [] if files == []: files = getKindleInfoFiles() for file in files: key = getDBfromFile(file) if key: # convert all values to hex, just in case. n_key = {} for k,v in key.items(): n_key[k.decode()]=codecs.encode(v, 'hex_codec').decode() # key = {k.decode():v.decode() for k,v in key.items()} keys.append(n_key) return keys # interface for Python DeDRM # returns single key or multiple keys, depending on path or file passed in def getkey(outpath, files=[]): keys = kindlekeys(files) if len(keys) > 0: if not os.path.isdir(outpath): outfile = outpath with open(outfile, 'w') as keyfileout: keyfileout.write(json.dumps(keys[0])) print("Saved a key to {0}".format(outfile)) else: keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(outpath,"kindlekey{0:d}.k4i".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'w') as keyfileout: keyfileout.write(json.dumps(key)) print("Saved a key to {0}".format(outfile)) return True return False def usage(progname): print("Finds, decrypts and saves the default Kindle For Mac/PC encryption keys.") print("Keys are saved to the current directory, or a specified output directory.") print("If a file name is passed instead of a directory, only the first key is saved, in that file.") print("Usage:") print(" {0:s} [-h] [-k ] []".format(progname)) def cli_main(): sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) argv=unicode_argv() progname = os.path.basename(argv[0]) print("{0} v{1}\nCopyright © 2010-2020 by some_updates, Apprentice Harper et al.".format(progname,__version__)) try: opts, args = getopt.getopt(argv[1:], "hk:") except getopt.GetoptError as err: print("Error in options or arguments: {0}".format(err.args[0])) usage(progname) sys.exit(2) files = [] for o, a in opts: if o == "-h": usage(progname) sys.exit(0) if o == "-k": files = [a] if len(args) > 1: usage(progname) sys.exit(2) if len(args) == 1: # save to the specified file or directory outpath = args[0] if not os.path.isabs(outpath): outpath = os.path.abspath(outpath) else: # save to the same directory as the script outpath = os.path.dirname(argv[0]) # make sure the outpath is canonical outpath = os.path.realpath(os.path.normpath(outpath)) if not getkey(outpath, files): print("Could not retrieve Kindle for Mac/PC key.") return 0 def gui_main(): try: import tkinter import tkinter.constants import tkinter.messagebox import traceback except: return cli_main() class ExceptionDialog(tkinter.Frame): def __init__(self, root, text): tkinter.Frame.__init__(self, root, border=5) label = tkinter.Label(self, text="Unexpected error:", anchor=tkinter.constants.W, justify=tkinter.constants.LEFT) label.pack(fill=tkinter.constants.X, expand=0) self.text = tkinter.Text(self) self.text.pack(fill=tkinter.constants.BOTH, expand=1) self.text.insert(tkinter.constants.END, text) argv=unicode_argv() root = tkinter.Tk() root.withdraw() progpath, progname = os.path.split(argv[0]) success = False try: keys = kindlekeys() keycount = 0 for key in keys: while True: keycount += 1 outfile = os.path.join(progpath,"kindlekey{0:d}.k4i".format(keycount)) if not os.path.exists(outfile): break with open(outfile, 'w') as keyfileout: keyfileout.write(json.dumps(key)) success = True tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile)) except DrmException as e: tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e))) except Exception: root.wm_state('normal') root.title(progname) text = traceback.format_exc() ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1) root.mainloop() if not success: return 1 return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main()) ================================================ FILE: DeDRM_plugin/kindlepid.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # Mobipocket PID calculator v0.4 for Amazon Kindle. # Copyright (c) 2007, 2009 Igor Skochinsky # History: # 0.1 Initial release # 0.2 Added support for generating PID for iPhone (thanks to mbp) # 0.3 changed to autoflush stdout, fixed return code usage # 0.3 updated for unicode # 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. # 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility # 1.0 Python 3 for calibre 5.0 import sys import binascii # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["kindlepid.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' def crc32(s): return (~binascii.crc32(s,-1))&0xFFFFFFFF def checksumPid(s): crc = crc32(s.encode('ascii')) crc = crc ^ (crc >> 16) res = s l = len(letters) for i in (0,1): b = crc & 0xff pos = (b // l) ^ (b % l) res += letters[pos%l] crc >>= 8 return res def pidFromSerial(s, l): crc = crc32(s) arr1 = [0]*l for i in range(len(s)): arr1[i%l] ^= s[i] crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] for i in range(l): arr1[i] ^= crc_bytes[i&3] pid = '' for i in range(l): b = arr1[i] & 0xff pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] return pid def cli_main(): print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky") argv=unicode_argv() if len(argv)==2: serial = argv[1] else: print("Usage: kindlepid.py /") return 1 if len(serial)==16: if serial.startswith("B") or serial.startswith("9"): print("Kindle serial number detected") else: print("Warning: unrecognized serial number. Please recheck input.") return 1 pid = pidFromSerial(serial.encode("utf-8"),7)+'*' print("Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid))) return 0 elif len(serial)==40: print("iPhone serial number (UDID) detected") pid = pidFromSerial(serial.encode("utf-8"),8) print("Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid))) return 0 print("Warning: unrecognized serial number. Please recheck input.") return 1 if __name__ == "__main__": sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) sys.exit(cli_main()) ================================================ FILE: DeDRM_plugin/mobidedrm.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # mobidedrm.py # Copyright © 2008 The Dark Reverser # Portions © 2008–2020 Apprentice Harper et al. from __future__ import print_function __license__ = 'GPL v3' __version__ = "1.0" # This is a python script. You need a Python interpreter to run it. # For example, ActiveState Python, which exists for windows. # # Changelog # 0.01 - Initial version # 0.02 - Huffdic compressed books were not properly decrypted # 0.03 - Wasn't checking MOBI header length # 0.04 - Wasn't sanity checking size of data record # 0.05 - It seems that the extra data flags take two bytes not four # 0.06 - And that low bit does mean something after all :-) # 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size # 0.08 - ...and also not in Mobi header version < 6 # 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! # 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre # import filter it works when importing unencrypted files. # Also now handles encrypted files that don't need a specific PID. # 0.11 - use autoflushed stdout and proper return values # 0.12 - Fix for problems with metadata import as Calibre plugin, report errors # 0.13 - Formatting fixes: retabbed file, removed trailing whitespace # and extra blank lines, converted CR/LF pairs at ends of each line, # and other cosmetic fixes. # 0.14 - Working out when the extra data flags are present has been problematic # Versions 7 through 9 have tried to tweak the conditions, but have been # only partially successful. Closer examination of lots of sample # files reveals that a confusion has arisen because trailing data entries # are not encrypted, but it turns out that the multibyte entries # in utf8 file are encrypted. (Although neither kind gets compressed.) # This knowledge leads to a simplification of the test for the # trailing data byte flags - version 5 and higher AND header size >= 0xE4. # 0.15 - Now outputs 'heartbeat', and is also quicker for long files. # 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. # 0.17 - added modifications to support its use as an imported python module # both inside calibre and also in other places (ie K4DeDRM tools) # 0.17a- disabled the standalone plugin feature since a plugin can not import # a plugin # 0.18 - It seems that multibyte entries aren't encrypted in a v7 file... # Removed the disabled Calibre plug-in code # Permit use of 8-digit PIDs # 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either. # 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file. # 0.21 - Added support for multiple pids # 0.22 - revised structure to hold MobiBook as a class to allow an extended interface # 0.23 - fixed problem with older files with no EXTH section # 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well # 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption # 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% # 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!) # 0.28 - slight additional changes to metadata token generation (None -> '') # 0.29 - It seems that the ideas about when multibyte trailing characters were # included in the encryption were wrong. They are for DOC compressed # files, but they are not for HUFF/CDIC compress files! # 0.30 - Modified interface slightly to work better with new calibre plugin style # 0.31 - The multibyte encrytion info is true for version 7 files too. # 0.32 - Added support for "Print Replica" Kindle ebooks # 0.33 - Performance improvements for large files (concatenation) # 0.34 - Performance improvements in decryption (libalfcrypto) # 0.35 - add interface to get mobi_version # 0.36 - fixed problem with TEXtREAd and getBookTitle interface # 0.37 - Fixed double announcement for stand-alone operation # 0.38 - Unicode used wherever possible, cope with absent alfcrypto # 0.39 - Fixed problem with TEXtREAd and getBookType interface # 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility # 0.41 - Fixed potential unicode problem in command line calls # 0.42 - Added GPL v3 licence. updated/removed some print statements # 1.0 - Python 3 compatibility for calibre 5.0 import sys import os import struct import binascii try: from alfcrypto import Pukall_Cipher except: print("AlfCrypto not found. Using python PC1 implementation.") # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get # encoded using "replace" before writing them. class SafeUnbuffered: def __init__(self, stream): self.stream = stream self.encoding = stream.encoding if self.encoding == None: self.encoding = "utf-8" def write(self, data): if isinstance(data, str): data = data.encode(self.encoding,"replace") self.stream.buffer.write(data) self.stream.buffer.flush() def __getattr__(self, attr): return getattr(self.stream, attr) iswindows = sys.platform.startswith('win') isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode # strings. # Versions 2.x of Python don't support Unicode in sys.argv on # Windows, with the underlying Windows API instead replacing multi-byte # characters with '?'. from ctypes import POINTER, byref, cdll, c_int, windll from ctypes.wintypes import LPCWSTR, LPWSTR GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] CommandLineToArgvW.restype = POINTER(LPWSTR) cmd = GetCommandLineW() argc = c_int(0) argv = CommandLineToArgvW(cmd, byref(argc)) if argc.value > 0: # Remove Python executable and commands if present start = argc.value - len(sys.argv) return [argv[i] for i in range(start, argc.value)] # if we don't have any arguments at all, just pass back script name # this should never happen return ["mobidedrm.py"] else: argvencoding = sys.stdin.encoding or "utf-8" return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv] class DrmException(Exception): pass # # MobiBook Utility Routines # # Implementation of Pukall Cipher 1 def PC1(key, src, decryption=True): # if we can get it from alfcrypto, use that try: return Pukall_Cipher().PC1(key,src,decryption) except NameError: pass except TypeError: pass # use slow python version, since Pukall_Cipher didn't load sum1 = 0; sum2 = 0; keyXorVal = 0; if len(key)!=16: DrmException ("PC1: Bad key length") wkey = [] for i in range(8): wkey.append(key[i*2]<<8 | key[i*2+1]) dst = b'' for i in range(len(src)): temp1 = 0; byteXorVal = 0; for j in range(8): temp1 ^= wkey[j] sum2 = (sum2+j)*20021 + sum1 sum1 = (temp1*346)&0xFFFF sum2 = (sum2+sum1)&0xFFFF temp1 = (temp1*20021+1)&0xFFFF byteXorVal ^= temp1 ^ sum2 curByte = src[i] if not decryption: keyXorVal = curByte * 257; curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF if decryption: keyXorVal = curByte * 257; for j in range(8): wkey[j] ^= keyXorVal; dst+=bytes([curByte]) return dst # accepts unicode returns unicode def checksumPid(s): letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF crc = crc ^ (crc >> 16) res = s l = len(letters) for i in (0,1): b = crc & 0xff pos = (b // l) ^ (b % l) res += letters[pos%l] crc >>= 8 return res # expects bytearray def getSizeOfTrailingDataEntries(ptr, size, flags): def getSizeOfTrailingDataEntry(ptr, size): bitpos, result = 0, 0 if size <= 0: return result while True: v = ptr[size-1] result |= (v & 0x7F) << bitpos bitpos += 7 size -= 1 if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): return result num = 0 testflags = flags >> 1 while testflags: if testflags & 1: num += getSizeOfTrailingDataEntry(ptr, size - num) testflags >>= 1 # Check the low bit to see if there's multibyte data present. # if multibyte data is included in the encryped data, we'll # have already cleared this flag. if flags & 1: num += (ptr[size - num - 1] & 0x3) + 1 return num class MobiBook: def loadSection(self, section): if (section + 1 == self.num_sections): endoff = len(self.data_file) else: endoff = self.sections[section + 1][0] off = self.sections[section][0] return self.data_file[off:endoff] def cleanup(self): # to match function in Topaz book pass def __init__(self, infile): print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__)) try: from alfcrypto import Pukall_Cipher except: print("AlfCrypto not found. Using python PC1 implementation.") # initial sanity check on file self.data_file = open(infile, 'rb').read() self.mobi_data = '' self.header = self.data_file[0:78] if self.header[0x3C:0x3C+8] != b'BOOKMOBI' and self.header[0x3C:0x3C+8] != b'TEXtREAd': raise DrmException("Invalid file format") self.magic = self.header[0x3C:0x3C+8] self.crypto_type = -1 # build up section offset and flag info self.num_sections, = struct.unpack('>H', self.header[76:78]) self.sections = [] for i in range(self.num_sections): offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8]) flags, val = a1, a2<<16|a3<<8|a4 self.sections.append( (offset, flags, val) ) # parse information from section 0 self.sect = self.loadSection(0) self.records, = struct.unpack('>H', self.sect[0x8:0x8+2]) self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2]) # det default values before PalmDoc test self.print_replica = False self.extra_data_flags = 0 self.meta_array = {} self.mobi_length = 0 self.mobi_codepage = 1252 self.mobi_version = -1 if self.magic == b'TEXtREAd': print("PalmDoc format book detected.") return self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) #print "MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length) if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) #print "Extra Data Flags: {0:d}".format(self.extra_data_flags) if (self.compression != 17480): # multibyte utf8 data is included in the encryption for PalmDoc compression # so clear that byte so that we leave it to be decrypted. self.extra_data_flags &= 0xFFFE # if exth region exists parse it for metadata array try: exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) exth = b'' if exth_flag & 0x40: exth = self.sect[16 + self.mobi_length:] if (len(exth) >= 12) and (exth[:4] == b'EXTH'): nitems, = struct.unpack('>I', exth[8:12]) pos = 12 for i in range(nitems): type, size = struct.unpack('>II', exth[pos: pos + 8]) content = exth[pos + 8: pos + size] self.meta_array[type] = content # reset the text to speech flag and clipping limit, if present if type == 401 and size == 9: # set clipping limit to 100% self.patchSection(0, b'\144', 16 + self.mobi_length + pos + 8) elif type == 404 and size == 9: # make sure text to speech is enabled self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8) # print type, size, content, content.encode('hex') pos += size except Exception as e: print("Cannot set meta_array: Error: {:s}".format(e.args[0])) #returns unicode def getBookTitle(self): codec_map = { 1252 : 'windows-1252', 65001 : 'utf-8', } title = b'' codec = 'windows-1252' if self.magic == b'BOOKMOBI': if 503 in self.meta_array: title = self.meta_array[503] else: toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c]) tend = toff + tlen title = self.sect[toff:tend] if self.mobi_codepage in codec_map.keys(): codec = codec_map[self.mobi_codepage] if title == b'': title = self.header[:32] title = title.split(b'\0')[0] return title.decode(codec) def getPIDMetaInfo(self): rec209 = b'' token = b'' if 209 in self.meta_array: rec209 = self.meta_array[209] data = rec209 # The 209 data comes in five byte groups. Interpret the last four bytes # of each group as a big endian unsigned integer to get a key value # if that key exists in the meta_array, append its contents to the token for i in range(0,len(data),5): val, = struct.unpack('>I',data[i+1:i+5]) sval = self.meta_array.get(val,b'') token += sval return rec209, token # new must be byte array def patch(self, off, new): self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] # new must be byte array def patchSection(self, section, new, in_off = 0): if (section + 1 == self.num_sections): endoff = len(self.data_file) else: endoff = self.sections[section + 1][0] off = self.sections[section][0] assert off + in_off + len(new) <= endoff self.patch(off + in_off, new) # pids in pidlist must be unicode, returned key is byte array, pid is unicode def parseDRM(self, data, count, pidlist): found_key = None keyvec1 = b'\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96' for pid in pidlist: bigpid = pid.encode('utf-8').ljust(16,b'\0') temp_key = PC1(keyvec1, bigpid, False) temp_key_sum = sum(temp_key) & 0xff found_key = None for i in range(count): verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) if cksum == temp_key_sum: cookie = PC1(temp_key, cookie) ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) if verification == ver and (flags & 0x1F) == 1: found_key = finalkey break if found_key != None: break if not found_key: # Then try the default encoding that doesn't require a PID pid = '00000000' temp_key = keyvec1 temp_key_sum = sum(temp_key) & 0xff for i in range(count): verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) if cksum == temp_key_sum: cookie = PC1(temp_key, cookie) ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) if verification == ver: found_key = finalkey break return [found_key,pid] def getFile(self, outpath): open(outpath,'wb').write(self.mobi_data) def getBookType(self): if self.print_replica: return "Print Replica" if self.mobi_version >= 8: return "Kindle Format 8" if self.mobi_version >= 0: return "Mobipocket {0:d}".format(self.mobi_version) return "PalmDoc" def getBookExtension(self): if self.print_replica: return ".azw4" if self.mobi_version >= 8: return ".azw3" return ".mobi" # pids in pidlist may be unicode or bytearrays or bytes def processBook(self, pidlist): crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) print("Crypto Type is: {0:d}".format(crypto_type)) self.crypto_type = crypto_type if crypto_type == 0: print("This book is not encrypted.") # we must still check for Print Replica self.print_replica = (self.loadSection(1)[0:4] == '%MOP') self.mobi_data = self.data_file return if crypto_type != 2 and crypto_type != 1: raise DrmException("Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type)) if 406 in self.meta_array: data406 = self.meta_array[406] val406, = struct.unpack('>Q',data406) if val406 != 0: raise DrmException("Cannot decode library or rented ebooks.") goodpids = [] # print("DEBUG ==== pidlist = ", pidlist) for pid in pidlist: if isinstance(pid,(bytearray,bytes)): pid = pid.decode('utf-8') if len(pid)==10: if checksumPid(pid[0:-2]) != pid: print("Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))) goodpids.append(pid[0:-2]) elif len(pid)==8: goodpids.append(pid) else: print("Warning: PID {0} has wrong number of digits".format(pid)) # print("======= DEBUG good pids = ", goodpids) if self.crypto_type == 1: t1_keyvec = b'QDCVEPMU675RUBSZ' if self.magic == b'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] elif self.mobi_version < 0: bookkey_data = self.sect[0x90:0x90+16] else: bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32] pid = '00000000' found_key = PC1(t1_keyvec, bookkey_data) else : # calculate the keys drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) if drm_count == 0: raise DrmException("Encryption not initialised. Must be opened with Mobipocket Reader first.") found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) if not found_key: raise DrmException("No key found in {0:d} PIDs tried.".format(len(goodpids))) # kill the drm keys self.patchSection(0, b'\0' * drm_size, drm_ptr) # kill the drm pointers self.patchSection(0, b'\xff' * 4 + b'\0' * 12, 0xA8) if pid=='00000000': print("File has default encryption, no specific key needed.") else: print("File is encoded with PID {0}.".format(checksumPid(pid))) # clear the crypto type self.patchSection(0, b'\0' * 2, 0xC) # decrypt sections print("Decrypting. Please wait . . .", end=' ') mobidataList = [] mobidataList.append(self.data_file[:self.sections[1][0]]) for i in range(1, self.records+1): data = self.loadSection(i) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) if i%100 == 0: print(".", end=' ') # print "record %d, extra_size %d" %(i,extra_size) decoded_data = PC1(found_key, data[0:len(data) - extra_size]) if i==1: self.print_replica = (decoded_data[0:4] == '%MOP') mobidataList.append(decoded_data) if extra_size > 0: mobidataList.append(data[-extra_size:]) if self.num_sections > self.records+1: mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) self.mobi_data = b''.join(mobidataList) print("done") return # pids in pidlist must be unicode def getUnencryptedBook(infile,pidlist): if not os.path.isfile(infile): raise DrmException("Input File Not Found.") book = MobiBook(infile) book.processBook(pidlist) return book.mobi_data def cli_main(): argv=unicode_argv() progname = os.path.basename(argv[0]) if len(argv)<3 or len(argv)>4: print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__)) print("Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks") print("Usage:") print(" {0} []".format(progname)) return 1 else: infile = argv[1] outfile = argv[2] if len(argv) == 4: pidlist = argv[3].split(',') else: pidlist = [] try: stripped_file = getUnencryptedBook(infile, pidlist) open(outfile, 'wb').write(stripped_file) except DrmException as e: print("MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])) return 1 return 0 if __name__ == '__main__': sys.stdout=SafeUnbuffered(sys.stdout) sys.stderr=SafeUnbuffered(sys.stderr) sys.exit(cli_main()) ================================================ FILE: DeDRM_plugin/openssl_des.py ================================================ #!/usr/bin/env python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # implement just enough of des from openssl to make erdr2pml.py happy def load_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library import sys if sys.platform.startswith('win'): libcrypto = find_library('libeay32') else: libcrypto = find_library('crypto') if libcrypto is None: return None libcrypto = CDLL(libcrypto) # typedef struct DES_ks # { # union # { # DES_cblock cblock; # /* make sure things are correct size on machines with # * 8 byte longs */ # DES_LONG deslong[2]; # } ks[16]; # } DES_key_schedule; # just create a big enough place to hold everything # it will have alignment of structure so we should be okay (16 byte aligned?) class DES_KEY_SCHEDULE(Structure): _fields_ = [('DES_cblock1', c_char * 16), ('DES_cblock2', c_char * 16), ('DES_cblock3', c_char * 16), ('DES_cblock4', c_char * 16), ('DES_cblock5', c_char * 16), ('DES_cblock6', c_char * 16), ('DES_cblock7', c_char * 16), ('DES_cblock8', c_char * 16), ('DES_cblock9', c_char * 16), ('DES_cblock10', c_char * 16), ('DES_cblock11', c_char * 16), ('DES_cblock12', c_char * 16), ('DES_cblock13', c_char * 16), ('DES_cblock14', c_char * 16), ('DES_cblock15', c_char * 16), ('DES_cblock16', c_char * 16)] DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p]) DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int]) class DES(object): def __init__(self, key): if len(key) != 8 : raise Exception('DES improper key used') return self.key = key self.keyschedule = DES_KEY_SCHEDULE() DES_set_key(self.key, self.keyschedule) def desdecrypt(self, data): ob = create_string_buffer(len(data)) DES_ecb_encrypt(data, ob, self.keyschedule, 0) return ob.raw def decrypt(self, data): if not data: return b'' i = 0 result = [] while i < len(data): block = data[i:i+8] processed_block = self.desdecrypt(block) result.append(processed_block) i += 8 return b''.join(result) return DES ================================================ FILE: DeDRM_plugin/plugin-import-name-dedrm.txt ================================================ ================================================ FILE: DeDRM_plugin/prefs.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai __license__ = 'GPL v3' # Standard Python modules. import os, sys, re, hashlib import codecs, json import traceback from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION from calibre.constants import iswindows, isosx class DeDRM_Prefs(): def __init__(self): JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') self.dedrmprefs = JSONConfig(JSON_PATH) self.dedrmprefs.defaults['configured'] = False self.dedrmprefs.defaults['bandnkeys'] = {} self.dedrmprefs.defaults['adeptkeys'] = {} self.dedrmprefs.defaults['ereaderkeys'] = {} self.dedrmprefs.defaults['kindlekeys'] = {} self.dedrmprefs.defaults['androidkeys'] = {} self.dedrmprefs.defaults['pids'] = [] self.dedrmprefs.defaults['serials'] = [] self.dedrmprefs.defaults['adobewineprefix'] = "" self.dedrmprefs.defaults['kindlewineprefix'] = "" # initialise # we must actually set the prefs that are dictionaries and lists # to empty dictionaries and lists, otherwise we are unable to add to them # as then it just adds to the (memory only) dedrmprefs.defaults versions! if self.dedrmprefs['bandnkeys'] == {}: self.dedrmprefs['bandnkeys'] = {} if self.dedrmprefs['adeptkeys'] == {}: self.dedrmprefs['adeptkeys'] = {} if self.dedrmprefs['ereaderkeys'] == {}: self.dedrmprefs['ereaderkeys'] = {} if self.dedrmprefs['kindlekeys'] == {}: self.dedrmprefs['kindlekeys'] = {} if self.dedrmprefs['androidkeys'] == {}: self.dedrmprefs['androidkeys'] = {} if self.dedrmprefs['pids'] == []: self.dedrmprefs['pids'] = [] if self.dedrmprefs['serials'] == []: self.dedrmprefs['serials'] = [] def __getitem__(self,kind = None): if kind is not None: return self.dedrmprefs[kind] return self.dedrmprefs def set(self, kind, value): self.dedrmprefs[kind] = value def writeprefs(self,value = True): self.dedrmprefs['configured'] = value def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): try: if keyvalue not in self.dedrmprefs[prefkind].values(): # ensure that the keyname is unique # by adding a number (starting with 2) to the name if it is not namecount = 1 newname = keyname while newname in self.dedrmprefs[prefkind]: namecount += 1 newname = "{0:s}_{1:d}".format(keyname,namecount) # add to the preferences self.dedrmprefs[prefkind][newname] = keyvalue return (True, newname) except: traceback.print_exc() pass return (False, keyname) def addvaluetoprefs(self, prefkind, prefsvalue): # ensure the keyvalue isn't already in the preferences try: if prefsvalue not in self.dedrmprefs[prefkind]: self.dedrmprefs[prefkind].append(prefsvalue) return True except: traceback.print_exc() return False def convertprefs(always = False): def parseIgnobleString(keystuff): from calibre_plugins.dedrm.ignoblekeygen import generate_key userkeys = [] ar = keystuff.split(':') for keystring in ar: try: name, ccn = keystring.split(',') # Generate Barnes & Noble EPUB user key from name and credit card number. keyname = "{0}_{1}".format(name.strip(),ccn.strip()[-4:]) keyvalue = generate_key(name, ccn) userkeys.append([keyname,keyvalue]) except Exception as e: traceback.print_exc() print(e.args[0]) pass return userkeys def parseeReaderString(keystuff): from calibre_plugins.dedrm.erdr2pml import getuser_key userkeys = [] ar = keystuff.split(':') for keystring in ar: try: name, cc = keystring.split(',') # Generate eReader user key from name and credit card number. keyname = "{0}_{1}".format(name.strip(),cc.strip()[-4:]) keyvalue = codecs.encode(getuser_key(name,cc),'hex') userkeys.append([keyname,keyvalue]) except Exception as e: traceback.print_exc() print(e.args[0]) pass return userkeys def parseKindleString(keystuff): pids = [] serials = [] ar = keystuff.split(',') for keystring in ar: keystring = str(keystring).strip().replace(" ","") if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids: pids.append(keystring) elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials: serials.append(keystring) return (pids,serials) def getConfigFiles(extension, encoding = None): # get any files with extension 'extension' in the config dir userkeys = [] files = [f for f in os.listdir(config_dir) if f.endswith(extension)] for filename in files: try: fpath = os.path.join(config_dir, filename) key = os.path.splitext(filename)[0] value = open(fpath, 'rb').read() if encoding is not None: value = codecs.encode(value,encoding) userkeys.append([key,value]) except: traceback.print_exc() pass return userkeys dedrmprefs = DeDRM_Prefs() if (not always) and dedrmprefs['configured']: # We've already converted old preferences, # and we're not being forced to do it again, so just return return print("{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION)) IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" EREADERPLUGINNAME = "eReader PDB 2 PML" OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" # get prefs from older tools kindleprefs = JSONConfig(os.path.join("plugins", "K4MobiDeDRM")) ignobleprefs = JSONConfig(os.path.join("plugins", "ignoble_epub_dedrm")) # Handle the old ignoble plugin's customization string by converting the # old string to stored keys... get that personal data out of plain sight. from calibre.customize.ui import config sc = config['plugin_customization'] val = sc.pop(IGNOBLEPLUGINNAME, None) if val is not None: print("{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)) priorkeycount = len(dedrmprefs['bandnkeys']) userkeys = parseIgnobleString(str(val)) for keypair in userkeys: name = keypair[0] value = keypair[1] dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount print("{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # Handle the old eReader plugin's customization string by converting the # old string to stored keys... get that personal data out of plain sight. val = sc.pop(EREADERPLUGINNAME, None) if val is not None: print("{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)) priorkeycount = len(dedrmprefs['ereaderkeys']) userkeys = parseeReaderString(str(val)) for keypair in userkeys: name = keypair[0] value = keypair[1] dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount print("{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # get old Kindle plugin configuration string val = sc.pop(OLDKINDLEPLUGINNAME, None) if val is not None: print("{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION)) priorpidcount = len(dedrmprefs['pids']) priorserialcount = len(dedrmprefs['serials']) pids, serials = parseKindleString(val) for pid in pids: dedrmprefs.addvaluetoprefs('pids',pid) for serial in serials: dedrmprefs.addvaluetoprefs('serials',serial) addedpidcount = len(dedrmprefs['pids']) - priorpidcount addedserialcount = len(dedrmprefs['serials']) - priorserialcount print("{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs", addedserialcount, "serial number" if addedserialcount==1 else "serial numbers")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext config['plugin_customization'] = sc # get any .b64 files in the config dir priorkeycount = len(dedrmprefs['bandnkeys']) bandnfilekeys = getConfigFiles('.b64') for keypair in bandnfilekeys: name = keypair[0] value = keypair[1] dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount if addedkeycount > 0: print("{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key file" if addedkeycount==1 else "key files")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # get any .der files in the config dir priorkeycount = len(dedrmprefs['adeptkeys']) adeptfilekeys = getConfigFiles('.der','hex') for keypair in adeptfilekeys: name = keypair[0] value = keypair[1] dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount if addedkeycount > 0: print("{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "keyfile" if addedkeycount==1 else "keyfiles")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # get ignoble json prefs if 'keys' in ignobleprefs: priorkeycount = len(dedrmprefs['bandnkeys']) for name in ignobleprefs['keys']: value = ignobleprefs['keys'][name] dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount # no need to delete old prefs, since they contain no recoverable private data if addedkeycount > 0: print("{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys")) # Make the json write all the prefs to disk dedrmprefs.writeprefs(False) # get kindle json prefs priorpidcount = len(dedrmprefs['pids']) priorserialcount = len(dedrmprefs['serials']) if 'pids' in kindleprefs: pids, serials = parseKindleString(kindleprefs['pids']) for pid in pids: dedrmprefs.addvaluetoprefs('pids',pid) if 'serials' in kindleprefs: pids, serials = parseKindleString(kindleprefs['serials']) for serial in serials: dedrmprefs.addvaluetoprefs('serials',serial) addedpidcount = len(dedrmprefs['pids']) - priorpidcount if addedpidcount > 0: print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs")) addedserialcount = len(dedrmprefs['serials']) - priorserialcount if addedserialcount > 0: print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, "serial number" if addedserialcount==1 else "serial numbers")) try: if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) print("{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix'])) except: traceback.print_exc() # Make the json write all the prefs to disk dedrmprefs.writeprefs() print("{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION)) ================================================ FILE: DeDRM_plugin/pycrypto_des.py ================================================ #!/usr/bin/env python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab def load_pycrypto(): try : from Crypto.Cipher import DES as _DES except: return None class DES(object): def __init__(self, key): if len(key) != 8 : raise ValueError('DES improper key used') self.key = key self._des = _DES.new(key,_DES.MODE_ECB) def desdecrypt(self, data): return self._des.decrypt(data) def decrypt(self, data): if not data: return '' i = 0 result = [] while i < len(data): block = data[i:i+8] processed_block = self.desdecrypt(block) result.append(processed_block) i += 8 return ''.join(result) return DES ================================================ FILE: DeDRM_plugin/python_des.py ================================================ #!/usr/bin/env python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import sys ECB = 0 CBC = 1 class Des(object): __pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3] __left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] __pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31] __ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6] __expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24, 23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0] __sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],] __p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24] __fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24] # Type of crypting being done ENCRYPT = 0x00 DECRYPT = 0x01 def __init__(self, key, mode=ECB, IV=None): if len(key) != 8: raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") self.block_size = 8 self.key_size = 8 self.__padding = '' self.setMode(mode) if IV: self.setIV(IV) self.L = [] self.R = [] self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) self.final = [] self.setKey(key) def getKey(self): return self.__key def setKey(self, key): self.__key = key self.__create_sub_keys() def getMode(self): return self.__mode def setMode(self, mode): self.__mode = mode def getIV(self): return self.__iv def setIV(self, IV): if not IV or len(IV) != self.block_size: raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") self.__iv = IV def getPadding(self): return self.__padding def __String_to_BitList(self, data): l = len(data) * 8 result = [0] * l pos = 0 for c in data: i = 7 ch = ord(c) while i >= 0: if ch & (1 << i) != 0: result[pos] = 1 else: result[pos] = 0 pos += 1 i -= 1 return result def __BitList_to_String(self, data): result = '' pos = 0 c = 0 while pos < len(data): c += data[pos] << (7 - (pos % 8)) if (pos % 8) == 7: result += chr(c) c = 0 pos += 1 return result def __permutate(self, table, block): return [block[x] for x in table] def __create_sub_keys(self): key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey())) i = 0 self.L = key[:28] self.R = key[28:] while i < 16: j = 0 while j < Des.__left_rotations[i]: self.L.append(self.L[0]) del self.L[0] self.R.append(self.R[0]) del self.R[0] j += 1 self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R) i += 1 def __des_crypt(self, block, crypt_type): block = self.__permutate(Des.__ip, block) self.L = block[:32] self.R = block[32:] if crypt_type == Des.ENCRYPT: iteration = 0 iteration_adjustment = 1 else: iteration = 15 iteration_adjustment = -1 i = 0 while i < 16: tempR = self.R[:] self.R = self.__permutate(Des.__expansion_table, self.R) self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])] B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] j = 0 Bn = [0] * 32 pos = 0 while j < 8: m = (B[j][0] << 1) + B[j][5] n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] v = Des.__sbox[j][(m << 4) + n] Bn[pos] = (v & 8) >> 3 Bn[pos + 1] = (v & 4) >> 2 Bn[pos + 2] = (v & 2) >> 1 Bn[pos + 3] = v & 1 pos += 4 j += 1 self.R = self.__permutate(Des.__p, Bn) self.R = [x ^ y for x, y in zip(self.R, self.L)] self.L = tempR i += 1 iteration += iteration_adjustment self.final = self.__permutate(Des.__fp, self.R + self.L) return self.final def crypt(self, data, crypt_type): if not data: return '' if len(data) % self.block_size != 0: if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") if not self.getPadding(): raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") else: data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() if self.getMode() == CBC: if self.getIV(): iv = self.__String_to_BitList(self.getIV()) else: raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") i = 0 dict = {} result = [] while i < len(data): block = self.__String_to_BitList(data[i:i+8]) if self.getMode() == CBC: if crypt_type == Des.ENCRYPT: block = [x ^ y for x, y in zip(block, iv)] processed_block = self.__des_crypt(block, crypt_type) if crypt_type == Des.DECRYPT: processed_block = [x ^ y for x, y in zip(processed_block, iv)] iv = block else: iv = processed_block else: processed_block = self.__des_crypt(block, crypt_type) result.append(self.__BitList_to_String(processed_block)) i += 8 if crypt_type == Des.DECRYPT and self.getPadding(): s = result[-1] while s[-1] == self.getPadding(): s = s[:-1] result[-1] = s return ''.join(result) def encrypt(self, data, pad=''): self.__padding = pad return self.crypt(data, Des.ENCRYPT) def decrypt(self, data, pad=''): self.__padding = pad return self.crypt(data, Des.DECRYPT) ================================================ FILE: DeDRM_plugin/scriptinterface.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import sys import os import re import traceback import calibre_plugins.dedrm.ineptepub import calibre_plugins.dedrm.ignobleepub import calibre_plugins.dedrm.epubtest import calibre_plugins.dedrm.zipfix import calibre_plugins.dedrm.ineptpdf import calibre_plugins.dedrm.erdr2pml import calibre_plugins.dedrm.k4mobidedrm def decryptepub(infile, outdir, rscpath): errlog = '' # first fix the epub to make sure we do not get errors name, ext = os.path.splitext(os.path.basename(infile)) bpath = os.path.dirname(infile) zippath = os.path.join(bpath,name + '_temp.zip') rv = zipfix.repairBook(infile, zippath) if rv != 0: print("Error while trying to fix epub") return rv # determine a good name for the output file outfile = os.path.join(outdir, name + '_nodrm.epub') rv = 1 # first try with the Adobe adept epub if ineptepub.adeptBook(zippath): # try with any keyfiles (*.der) in the rscpath files = os.listdir(rscpath) filefilter = re.compile("\.der$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: keypath = os.path.join(rscpath, filename) userkey = open(keypath,'rb').read() try: rv = ineptepub.decryptBook(userkey, zippath, outfile) if rv == 0: print("Decrypted Adobe ePub with key file {0}".format(filename)) break except Exception as e: errlog += traceback.format_exc() errlog += str(e) rv = 1 # now try with ignoble epub elif ignobleepub.ignobleBook(zippath): # try with any keyfiles (*.b64) in the rscpath files = os.listdir(rscpath) filefilter = re.compile("\.b64$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: keypath = os.path.join(rscpath, filename) userkey = open(keypath,'r').read() #print userkey try: rv = ignobleepub.decryptBook(userkey, zippath, outfile) if rv == 0: print("Decrypted B&N ePub with key file {0}".format(filename)) break except Exception as e: errlog += traceback.format_exc() errlog += str(e) rv = 1 else: encryption = epubtest.encryption(zippath) if encryption == "Unencrypted": print("{0} is not DRMed.".format(name)) rv = 0 else: print("{0} has an unknown encryption.".format(name)) os.remove(zippath) if rv != 0: print(errlog) return rv def decryptpdf(infile, outdir, rscpath): errlog = '' rv = 1 # determine a good name for the output file name, ext = os.path.splitext(os.path.basename(infile)) outfile = os.path.join(outdir, name + '_nodrm.pdf') # try with any keyfiles (*.der) in the rscpath files = os.listdir(rscpath) filefilter = re.compile("\.der$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: keypath = os.path.join(rscpath, filename) userkey = open(keypath,'rb').read() try: rv = ineptpdf.decryptBook(userkey, infile, outfile) if rv == 0: break except Exception as e: errlog += traceback.format_exc() errlog += str(e) rv = 1 if rv != 0: print(errlog) return rv def decryptpdb(infile, outdir, rscpath): errlog = '' outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz" outpath = os.path.join(outdir, outname) rv = 1 socialpath = os.path.join(rscpath,'sdrmlist.txt') if os.path.exists(socialpath): keydata = file(socialpath,'r').read() keydata = keydata.rstrip(os.linesep) ar = keydata.split(',') for i in ar: try: name, cc8 = i.split(':') except ValueError: print(' Error parsing user supplied social drm data.') return 1 try: rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) except Exception as e: errlog += traceback.format_exc() errlog += str(e) rv = 1 if rv == 0: break return rv def decryptk4mobi(infile, outdir, rscpath): errlog = '' rv = 1 pidnums = [] pidspath = os.path.join(rscpath,'pidlist.txt') if os.path.exists(pidspath): pidstr = file(pidspath,'r').read() pidstr = pidstr.rstrip(os.linesep) pidstr = pidstr.strip() if pidstr != '': pidnums = pidstr.split(',') serialnums = [] serialnumspath = os.path.join(rscpath,'seriallist.txt') if os.path.exists(serialnumspath): serialstr = file(serialnumspath,'r').read() serialstr = serialstr.rstrip(os.linesep) serialstr = serialstr.strip() if serialstr != '': serialnums = serialstr.split(',') kDatabaseFiles = [] files = os.listdir(rscpath) filefilter = re.compile("\.k4i$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: dpath = os.path.join(rscpath,filename) kDatabaseFiles.append(dpath) androidFiles = [] files = os.listdir(rscpath) filefilter = re.compile("\.ab$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: dpath = os.path.join(rscpath,filename) androidFiles.append(dpath) files = os.listdir(rscpath) filefilter = re.compile("\.db$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: dpath = os.path.join(rscpath,filename) androidFiles.append(dpath) files = os.listdir(rscpath) filefilter = re.compile("\.xml$", re.IGNORECASE) files = filter(filefilter.search, files) if files: for filename in files: dpath = os.path.join(rscpath,filename) androidFiles.append(dpath) try: rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums) except Exception as e: errlog += traceback.format_exc() errlog += str(e) rv = 1 return rv ================================================ FILE: DeDRM_plugin/scrolltextwidget.py ================================================ #!/usr/bin/env python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import tkinter import tkinter.constants # basic scrolled text widget class ScrolledText(tkinter.Text): def __init__(self, master=None, **kw): self.frame = tkinter.Frame(master) self.vbar = tkinter.Scrollbar(self.frame) self.vbar.pack(side=tkinter.constants.RIGHT, fill=tkinter.constants.Y) kw.update({'yscrollcommand': self.vbar.set}) tkinter.Text.__init__(self, self.frame, **kw) self.pack(side=tkinter.constants.LEFT, fill=tkinter.constants.BOTH, expand=True) self.vbar['command'] = self.yview # Copy geometry methods of self.frame without overriding Text # methods = hack! text_meths = list(vars(tkinter.Text).keys()) methods = list(vars(tkinter.Pack).keys()) + list(vars(tkinter.Grid).keys()) + list(vars(tkinter.Place).keys()) methods = set(methods).difference(text_meths) for m in methods: if m[0] != '_' and m != 'config' and m != 'configure': setattr(self, m, getattr(self.frame, m)) def __str__(self): return str(self.frame) ================================================ FILE: DeDRM_plugin/simpleprefs.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab import sys import os, os.path import shutil class SimplePrefsError(Exception): pass class SimplePrefs(object): def __init__(self, target, description): self.prefs = {} self.key2file={} self.file2key={} for keyfilemap in description: [key, filename] = keyfilemap self.key2file[key] = filename self.file2key[filename] = key self.target = target + 'Prefs' if sys.platform.startswith('win'): import winreg regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") path = winreg.QueryValueEx(regkey, 'Local AppData')[0] prefdir = path + os.sep + self.target elif sys.platform.startswith('darwin'): home = os.getenv('HOME') prefdir = os.path.join(home,'Library','Preferences','org.' + self.target) else: # linux and various flavors of unix home = os.getenv('HOME') prefdir = os.path.join(home,'.' + self.target) if not os.path.exists(prefdir): os.makedirs(prefdir) self.prefdir = prefdir self.prefs['dir'] = self.prefdir self._loadPreferences() def _loadPreferences(self): filenames = os.listdir(self.prefdir) for filename in filenames: if filename in self.file2key: key = self.file2key[filename] filepath = os.path.join(self.prefdir,filename) if os.path.isfile(filepath): try : data = file(filepath,'rb').read() self.prefs[key] = data except Exception as e: pass def getPreferences(self): return self.prefs def setPreferences(self, newprefs={}): if 'dir' not in newprefs: raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') if newprefs['dir'] != self.prefs['dir']: raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory') for key in newprefs: if key != 'dir': if key in self.key2file: filename = self.key2file[key] filepath = os.path.join(self.prefdir,filename) data = newprefs[key] if data != None: data = str(data) if data == None or data == '': if os.path.exists(filepath): os.remove(filepath) else: try: file(filepath,'wb').write(data) except Exception as e: pass self.prefs = newprefs return ================================================ FILE: DeDRM_plugin/stylexml2css.py ================================================ #! /usr/bin/python # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # For use with Topaz Scripts Version 2.6 import csv import sys import os import getopt import re from struct import pack from struct import unpack debug = False class DocParser(object): def __init__(self, flatxml, fontsize, ph, pw): self.flatdoc = flatxml.split(b'\n') self.fontsize = int(fontsize) self.ph = int(ph) * 1.0 self.pw = int(pw) * 1.0 stags = { b'paragraph' : 'p', b'graphic' : '.graphic' } attr_val_map = { b'hang' : 'text-indent: ', b'indent' : 'text-indent: ', b'line-space' : 'line-height: ', b'margin-bottom' : 'margin-bottom: ', b'margin-left' : 'margin-left: ', b'margin-right' : 'margin-right: ', b'margin-top' : 'margin-top: ', b'space-after' : 'padding-bottom: ', } attr_str_map = { b'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;', b'align-left' : 'text-align: left;', b'align-right' : 'text-align: right;', b'align-justify' : 'text-align: justify;', b'display-inline' : 'display: inline;', b'pos-left' : 'text-align: left;', b'pos-right' : 'text-align: right;', b'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;', } # find tag if within pos to end inclusive def findinDoc(self, tagpath, pos, end) : result = None docList = self.flatdoc cnt = len(docList) if end == -1 : end = cnt else: end = min(cnt,end) foundat = -1 for j in range(pos, end): item = docList[j] if item.find(b'=') >= 0: (name, argres) = item.split(b'=',1) else : name = item argres = b'' if (isinstance(tagpath,str)): tagpath = tagpath.encode('utf-8') if name.endswith(tagpath) : result = argres foundat = j break return foundat, result # return list of start positions for the tagpath def posinDoc(self, tagpath): startpos = [] pos = 0 res = b"" while res != None : (foundpos, res) = self.findinDoc(tagpath, pos, -1) if res != None : startpos.append(foundpos) pos = foundpos + 1 return startpos # returns a vector of integers for the tagpath def getData(self, tagpath, pos, end, clean=False): if clean: digits_only = re.compile(rb'''([0-9]+)''') argres=[] (foundat, argt) = self.findinDoc(tagpath, pos, end) if (argt != None) and (len(argt) > 0) : argList = argt.split(b'|') for strval in argList: if clean: m = re.search(digits_only, strval) if m != None: strval = m.group() argres.append(int(strval)) return argres def process(self): classlst = '' csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n' csspage += '.cl-right { text-align: right; }\n' csspage += '.cl-left { text-align: left; }\n' csspage += '.cl-justify { text-align: justify; }\n' # generate a list of each