├── .github ├── ISSUE_TEMPLATE │ └── QUESTION.yml └── workflows │ └── main.yml ├── .gitignore ├── CALIBRE_CLI_INSTRUCTIONS.md ├── CHANGELOG.md ├── DeDRM_plugin ├── DeDRM_ADE PassHash Key_Help.htm ├── DeDRM_Adobe Digital Editions 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_PDF passphrase_Help.htm ├── DeDRM_Readium LCP passphrase_Help.htm ├── DeDRM_eReader Key_Help.htm ├── __calibre_compat_code.py ├── __init__.py ├── __main__.py ├── __version.py ├── adobekey.py ├── adobekey_get_passhash.py ├── adobekey_winreg_unicode.py ├── aescbc.py ├── alfcrypto.py ├── androidkindlekey.py ├── argv_utils.py ├── config.py ├── convert2xml.py ├── epubfontdecrypt.py ├── epubtest.py ├── epubwatermark.py ├── erdr2pml.py ├── flatxml2html.py ├── flatxml2svg.py ├── genbook.py ├── ignoblekeyAndroid.py ├── ignoblekeyGenPassHash.py ├── ignoblekeyNookStudy.py ├── ignoblekeyWindowsStore.py ├── ineptepub.py ├── ineptpdf.py ├── ion.py ├── k4mobidedrm.py ├── kfxdedrm.py ├── kfxtables.py ├── kgenpids.py ├── kindlekey.py ├── kindlepid.py ├── lcpdedrm.py ├── mobidedrm.py ├── plugin-import-name-dedrm.txt ├── prefs.py ├── scriptinterface.py ├── standalone │ ├── __init__.py │ ├── jsonconfig.py │ ├── passhash.py │ └── remove_drm.py ├── stylexml2css.py ├── topazextract.py ├── utilities.py ├── wineutils.py ├── zeroedzipinfo.py ├── zipfilerugged.py └── zipfix.py ├── DeDRM_plugin_ReadMe.txt ├── FAQs.md ├── Obok_plugin ├── __init__.py ├── action.py ├── common_utils.py ├── config.py ├── dialogs.py ├── images │ └── obok.png ├── 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.zip │ └── rebhack_ReadMe.txt ├── Scuolabook_DRM │ └── Scuolabook_ReadMe.txt └── Tetrachroma_FileOpen_ineptpdf │ ├── ineptpdf_8.4.51_ReadMe.txt │ └── ineptpdf_fileopen.pyw ├── README.md ├── ReadMe_Overview.txt ├── make_release.py └── obok_plugin_ReadMe.txt /.github/ISSUE_TEMPLATE/QUESTION.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Questions for DeDRM Project 3 | body: 4 | - type: textarea 5 | id: question 6 | attributes: 7 | label: Question / bug report 8 | description: Please enter your question / your bug report. 9 | - type: input 10 | id: calibre-version 11 | attributes: 12 | label: Which version of Calibre are you running? 13 | description: "Example: 6.23" 14 | placeholder: "6.23" 15 | validations: 16 | required: true 17 | - type: input 18 | id: plugin-version 19 | attributes: 20 | label: Which version of the DeDRM plugin are you running? 21 | description: "Example: v10.0.2" 22 | placeholder: "v10.0.2" 23 | validations: 24 | required: true 25 | - type: input 26 | id: kindle-version 27 | attributes: 28 | label: If applicable, which version of the Kindle software are you running? 29 | description: "Example: 1.24" 30 | placeholder: "Leave empty if unrelated to Kindle books" 31 | validations: 32 | required: false 33 | - type: textarea 34 | id: log 35 | attributes: 36 | label: Log output 37 | description: If applicable, please post your log output here - into the code block. 38 | value: | 39 | ```log 40 | Paste log output here. 41 | ``` -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Package plugin 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | package: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | 13 | - name: Package 14 | run: python3 make_release.py 15 | 16 | - name: Upload 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: plugin 20 | path: | 21 | DeDRM_tools_*.zip 22 | DeDRM_tools.zip 23 | 24 | - name: Prepare release 25 | run: cp DeDRM_tools.zip DeDRM_alpha_${{ github.sha }}.zip 26 | 27 | 28 | - uses: dev-drprasad/delete-older-releases@v0.2.1 29 | with: 30 | repo: noDRM/DeDRM_tools_autorelease 31 | keep_latest: 0 32 | delete_tags: true 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.AUTORELEASE_KEY }} 35 | 36 | - name: Auto-release 37 | id: autorelease 38 | uses: softprops/action-gh-release@v1 39 | with: 40 | tag_name: autorelease_${{ github.sha }} 41 | repository: noDRM/DeDRM_tools_autorelease 42 | token: ${{ secrets.AUTORELEASE_KEY }} 43 | name: Automatic alpha release with latest changes 44 | body: | 45 | This release is automatically generated by Github for each commit. 46 | 47 | This means, every time a change is made to the repo, a release with an untested copy of the plugin at that stage will be created. This will contain the most up-to-date code, but it's not tested at all and may be broken. 48 | 49 | Last update based on Git commit [${{ github.sha }}](https://github.com/noDRM/DeDRM_tools/commit/${{ github.sha }}). 50 | prerelease: true 51 | draft: false 52 | files: DeDRM_alpha_${{ github.sha }}.zip 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac files 2 | .DS_Store 3 | 4 | # local test data 5 | /user_data/ 6 | 7 | # Cache 8 | /DeDRM_plugin/__pycache__ 9 | /DeDRM_plugin/standalone/__pycache__ -------------------------------------------------------------------------------- /CALIBRE_CLI_INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | # Using the DeDRM plugin with the Calibre command line interface 2 | 3 | If you prefer the Calibre CLI instead of the GUI, follow this guide to 4 | install and use the DeDRM plugin. 5 | 6 | This guide assumes you are on Linux, but it may very well work on other 7 | platforms. 8 | 9 | ## Step-by-step Tutorial 10 | 11 | #### Install Calibre 12 | - Follow [Calibre's installation instructions](https://calibre-ebook.com/download_linux) 13 | 14 | #### Install plugins 15 | - Download the DeDRM `.zip` archive from DeDRM_tools' 16 | [latest release](https://github.com/noDRM/DeDRM_tools/releases/latest). 17 | Then unzip it. 18 | - Add the DeDRM plugin to Calibre: 19 | ``` 20 | cd *the unzipped DeDRM_tools folder* 21 | calibre-customize --add DeDRM_plugin.zip 22 | ``` 23 | - Add the Obok plugin: 24 | ``` 25 | calibre-customize --add Obok_plugin.zip 26 | ``` 27 | 28 | #### Enter your keys 29 | - Figure out what format DeDRM wants your key in by looking in 30 | [the code that handles that](DeDRM_plugin/prefs.py). 31 | - For Kindle eInk devices, DeDRM expects you to put a list of serial 32 | numbers in the `serials` field: `"serials": ["012345689abcdef"]` or 33 | `"serials": ["1111111111111111", "2222222222222222"]`. 34 | - Now add your keys to `$CALIBRE_CONFIG_DIRECTORY/plugins/dedrm.json`. 35 | 36 | #### Import your books 37 | - Make a library folder 38 | ``` 39 | mkdir library 40 | ``` 41 | - Add your book(s) with this command: 42 | ``` 43 | calibredb add /path/to/book.format --with-library=library 44 | ``` 45 | 46 | The DRM should be removed from your book, which you can find in the `library` 47 | folder. 48 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing Adobe PassHash (B&N) Keys 9 | 16 | 17 | 18 | 19 | 20 |

Managing Adobe PassHash Keys

21 | 22 |

Adobe PassHash is a variant of the Adobe DRM which is used by retailers like Barnes and Noble. Instead of using certificates and device-based authorization, this uses a username and password combination. In B&&Ns implementation however, the user never gets access to these credentials, just to the credential hash.

23 | 24 |

Changes at Barnes & Noble

25 | 26 |

Since 2014, Barnes & Noble is no longer using the default Adobe key generation algorithm, which used to be the full name as "username" and the full credit card number as "password" for the PassHash algorithm. 27 | Instead, they started generating a random key on their server and send that to the reading application during login. This means that the old method to decrypt these books will no longer work.

28 | 29 |

There used to be a way to use the Android app's API to simulate a login to the Barnes and Noble servers, but that API has been shut down a while ago, too, and so far nobody has reverse-engineered the new one.

30 | 31 |

Importing PassHash / B&N keys

32 | 33 |

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.

34 |

Currently, the only known ways to access the key are the following:

35 | 43 | 44 | 45 |

After you've selected a key retrieval method from the settings, the dialog may change and request some additional information depending on the key retrieval method. Enter that, then click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

46 |

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

47 | 48 |

Deleting Keys:

49 | 50 |

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.

51 | 52 |

Renaming Keys:

53 | 54 |

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..

55 | 56 |

Exporting Keys:

57 | 58 |

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.

59 | 60 |

Importing Existing Keyfiles:

61 | 62 |

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.

63 | 64 |

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.

65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing Adobe Digital Editions Keys 9 | 16 | 17 | 18 | 19 | 20 |

Managing Adobe Digital Editions Keys

21 | 22 | 23 |

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.

24 | 25 |

Creating New Keys:

26 | 27 |

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.

28 | 31 | 32 |

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.

33 |

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

34 | 35 |

Deleting Keys:

36 | 37 |

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.

38 | 39 |

Renaming Keys:

40 | 41 |

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..

42 | 43 |

Exporting Keys:

44 | 45 |

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.

46 | 47 |

Linux Users: WINEPREFIX

48 | 49 |

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.

50 | 51 |

Importing Existing Keyfiles:

52 | 53 |

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.

54 | 55 |

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.

56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | Managing eInk Kindle serial numbers 9 | 16 | 17 | 18 | 19 | 20 |

Managing eInk Kindle serial numbers

21 | 22 |

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.

23 | 24 |

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.

25 | 26 |

Creating New Kindle serial numbers:

27 | 28 |

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.

29 | 32 | 33 |

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

34 | 35 |

Deleting Kindle serial numbers:

36 | 37 |

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.

38 | 39 |

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.

40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | DeDRM Plugin Configuration 9 | 16 | 17 | 18 | 19 | 20 |

DeDRM Plugin (v10.0.9 / v10.1.0 RC1)

21 | 22 |

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.

23 | 24 |

It is a forked version created by NoDRM, based on the original plugin by Apprentice Alf and Apprentice Harper.

25 | 26 |

Installation

27 |

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).

28 | 29 |

This plugin (in versions v10.0.0 and above) will automatically replace the older 7.X and below versions from Apprentice Alf and Apprentice Harper.

30 | 31 |

Configuration

32 |

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).

33 | 34 |

If you are using the DeACSM / ACSM Input Plugin for Calibre, the keys will also automatically be dumped for you.

35 | 36 |

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.

37 | 38 |

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.

39 | 40 |

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.

41 | 42 |

Troubleshooting:

43 | 44 |

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.

45 | 46 |

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.

47 |

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.

48 | 49 |

Credits:

50 | 64 | 65 |

For additional help read the FAQs at NoDRM's GitHub repository (or the corresponding FAQs at Apprentice Harpers’s GitHub repository). You can open issue reports related to this fork at NoDRM's GitHub repository.

66 | 67 | 68 |

Linux Systems Only

69 |

Generating decryption keys for Adobe Digital Editions and Kindle for PC

70 |

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.

71 | 72 |

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.)

73 | 74 |

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.

75 | 76 |

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.

77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | Managing Kindle for Android Keys 9 | 16 | 17 | 18 | 19 | 20 |

Managing Kindle for Android Keys

21 | 22 |

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.

23 | 24 |

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

25 | 26 |

Getting the Kindle for Android backup file

27 | 28 |

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.

29 |

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

30 |

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. 31 | 32 |

Adding a Kindle for Android Key

33 | 34 |

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. 35 |

39 | 40 |

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.

41 |

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

42 | 43 |

Deleting Keys:

44 | 45 |

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.

46 | 47 |

Renaming Keys:

48 | 49 |

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.

50 | 51 |

Exporting Keys:

52 | 53 |

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.

54 | 55 |

Importing Existing Keyfiles:

56 | 57 |

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.

58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing Kindle for Mac/PC Keys 9 | 16 | 17 | 18 | 19 | 20 |

Managing Kindle for Mac/PC Keys

21 | 22 | 23 |

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.

24 | 25 |

Note that for best results, you should run Calibre / this plugin on the same machine where Kindle 4 PC / Kindle 4 Mac is running. It is possible to export/import the keys to another machine, but this may not always work, particularly with the newer DRM versions.

26 | 27 |

Creating New Keys:

28 | 29 |

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.

30 | 33 | 34 |

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.

35 |

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

36 | 37 |

Deleting Keys:

38 | 39 |

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.

40 | 41 |

Renaming Keys:

42 | 43 |

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..

44 | 45 |

Exporting Keys:

46 | 47 |

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.

48 | 49 |

Linux Users: WINEPREFIX

50 | 51 |

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.

52 | 53 |

Importing Existing Keyfiles:

54 | 55 |

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.

56 | 57 |

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.

58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing Mobipocket PIDs 9 | 16 | 17 | 18 | 19 | 20 |

Managing Mobipocket PIDs

21 | 22 |

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.

23 | 24 | 25 |

Creating New Mobipocket PIDs:

26 | 27 |

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.

28 | 31 | 32 |

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

33 | 34 |

Deleting Mobipocket PIDs:

35 | 36 |

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.

37 | 38 |

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

39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_PDF passphrase_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing PDF passwords 9 | 16 | 17 | 18 | 19 | 20 |

Managing PDF passwords

21 | 22 |

PDF files can be protected with a password / passphrase that will be required to open the PDF file. Enter your passphrases in the plugin settings to have the plugin automatically remove this encryption / restriction from PDF files you import.

23 | 24 | 25 |

Entering a passphrase:

26 | 27 |

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 passphrase.

28 | 29 |

Just enter your passphrase for the PDF file, then click the OK button to save the passphrase.

30 | 31 |

Deleting a passphrase:

32 | 33 |

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 passphrase 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.

34 | 35 |

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

36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_Readium LCP passphrase_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing Readium LCP passphrases 9 | 16 | 17 | 18 | 19 | 20 |

Managing Readium LCP passphrases

21 | 22 |

Readium LCP is a relatively new eBook DRM. It's also known under the names "CARE DRM" or "TEA DRM". It does not rely on any accounts or key data that's difficult to acquire. All you need to open (or decrypt) LCP eBooks is the account passphrase given to you by the eBook provider - the very same passphrase you'd have to enter into your eBook reader device (once) to read LCP-encrypted books.

23 | 24 |

This plugin no longer supports removing the Readium LCP DRM due to a DMCA takedown request issued by Readium. Please read the takedown notice or this bug report for more information.

25 | 26 |

Entering an LCP passphrase:

27 | 28 |

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 passphrase.

29 | 30 |

Just enter your passphrase as provided with the book, then click the OK button to save the passphrase.

31 | 32 |

Usually, passphrases are identical for all books bought with the same account. So if you buy multiple LCP-protected eBooks, they'll usually all have the same passphrase if they've all been bought at the same store with the same account.

33 | 34 |

Deleting an LCP passphrase:

35 | 36 |

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 passphrase 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.

37 | 38 |

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

39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /DeDRM_plugin/DeDRM_eReader Key_Help.htm: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Managing eReader Keys 9 | 16 | 17 | 18 | 19 | 20 |

Managing eReader Keys

21 | 22 |

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.

23 | 24 |

Creating New Keys:

25 | 26 |

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.

27 | 32 | 33 |

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

34 |

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

35 | 36 |

Deleting Keys:

37 | 38 |

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.

39 | 40 |

Renaming Keys:

41 | 42 |

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..

43 | 44 |

Exporting Keys:

45 | 46 |

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.

47 | 48 |

Importing Existing Keyfiles:

49 | 50 |

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.

51 | 52 |

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.

53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /DeDRM_plugin/__calibre_compat_code.py: -------------------------------------------------------------------------------- 1 | 2 | #@@CALIBRE_COMPAT_CODE_START@@ 3 | import sys, os 4 | 5 | # Explicitly allow importing everything ... 6 | if os.path.dirname(os.path.dirname(os.path.abspath(__file__))) not in sys.path: 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | if os.path.dirname(os.path.abspath(__file__)) not in sys.path: 9 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 10 | 11 | # Bugfix for Calibre < 5: 12 | if "calibre" in sys.modules and sys.version_info[0] == 2: 13 | from calibre.utils.config import config_dir 14 | if os.path.join(config_dir, "plugins", "DeDRM.zip") not in sys.path: 15 | sys.path.insert(0, os.path.join(config_dir, "plugins", "DeDRM.zip")) 16 | 17 | if "calibre" in sys.modules: 18 | # Explicitly set the package identifier so we are allowed to import stuff ... 19 | __package__ = "calibre_plugins.dedrm" 20 | 21 | #@@CALIBRE_COMPAT_CODE_END@@ 22 | -------------------------------------------------------------------------------- /DeDRM_plugin/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # __main__.py for DeDRM_plugin 5 | # (CLI interface without Calibre) 6 | # Copyright © 2021 NoDRM 7 | 8 | """ 9 | 10 | NOTE: This code is not functional (yet). I started working on it a while ago 11 | to make a standalone version of the plugins that could work without Calibre, 12 | too, but for now there's only a rough code structure and no working code yet. 13 | 14 | Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll 15 | change in the future. 16 | 17 | """ 18 | 19 | __license__ = 'GPL v3' 20 | __docformat__ = 'restructuredtext en' 21 | 22 | # For revision history see CHANGELOG.md 23 | 24 | """ 25 | Run DeDRM plugin without Calibre. 26 | """ 27 | 28 | # Import __init__.py from the standalone folder so we can have all the 29 | # standalone / non-Calibre code in that subfolder. 30 | 31 | import standalone.__init__ as mdata 32 | import sys 33 | 34 | mdata.main(sys.argv) -------------------------------------------------------------------------------- /DeDRM_plugin/__version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | #@@CALIBRE_COMPAT_CODE@@ 5 | 6 | PLUGIN_NAME = "DeDRM" 7 | __version__ = '10.0.9' 8 | 9 | PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")]) 10 | PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE]) 11 | # Include an html helpfile in the plugin's zipfile with the following name. 12 | RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' -------------------------------------------------------------------------------- /DeDRM_plugin/adobekey_get_passhash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # adobekey_get_passhash.py, version 1 5 | # based on adobekey.pyw, version 7.2 6 | # Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al. 7 | # Copyright © 2021 noDRM 8 | 9 | # Released under the terms of the GNU General Public Licence, version 3 10 | # 11 | 12 | # Revision history: 13 | # 1 - Initial release 14 | 15 | """ 16 | Retrieve Adobe ADEPT user passhash keys 17 | """ 18 | 19 | __license__ = 'GPL v3' 20 | __version__ = '1' 21 | 22 | import sys, os, time 23 | import base64, hashlib 24 | try: 25 | from Cryptodome.Cipher import AES 26 | except ImportError: 27 | from Crypto.Cipher import AES 28 | 29 | 30 | def unpad(data, padding=16): 31 | if sys.version_info[0] == 2: 32 | pad_len = ord(data[-1]) 33 | else: 34 | pad_len = data[-1] 35 | 36 | return data[:-pad_len] 37 | 38 | PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" 39 | 40 | 41 | try: 42 | from calibre.constants import iswindows, isosx 43 | except: 44 | iswindows = sys.platform.startswith('win') 45 | isosx = sys.platform.startswith('darwin') 46 | 47 | 48 | class ADEPTError(Exception): 49 | pass 50 | 51 | def decrypt_passhash(passhash, fp): 52 | 53 | serial_number = base64.b64decode(fp).hex() 54 | 55 | hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16] 56 | 57 | encrypted_cc_hash = base64.b64decode(passhash) 58 | cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:])) 59 | return base64.b64encode(cc_hash).decode("ascii") 60 | 61 | 62 | if iswindows: 63 | try: 64 | import winreg 65 | except ImportError: 66 | import _winreg as winreg 67 | 68 | PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation' 69 | 70 | def passhash_keys(): 71 | cuser = winreg.HKEY_CURRENT_USER 72 | keys = [] 73 | names = [] 74 | try: 75 | plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) 76 | except WindowsError: 77 | raise ADEPTError("Could not locate ADE activation") 78 | except FileNotFoundError: 79 | raise ADEPTError("Could not locate ADE activation") 80 | 81 | idx = 1 82 | 83 | fp = None 84 | 85 | i = -1 86 | while True: 87 | i = i + 1 # start with 0 88 | try: 89 | plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) 90 | except: 91 | # No more keys 92 | break 93 | 94 | ktype = winreg.QueryValueEx(plkparent, None)[0] 95 | 96 | if ktype == "activationToken": 97 | # find fingerprint for hash decryption 98 | j = -1 99 | while True: 100 | j = j + 1 # start with 0 101 | try: 102 | plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) 103 | except WindowsError: 104 | break 105 | except FileNotFoundError: 106 | break 107 | ktype = winreg.QueryValueEx(plkkey, None)[0] 108 | if ktype == 'fingerprint': 109 | fp = winreg.QueryValueEx(plkkey, 'value')[0] 110 | #print("Found fingerprint: " + fp) 111 | 112 | 113 | # Note: There can be multiple lists, with multiple entries each. 114 | if ktype == 'passHashList': 115 | 116 | # Find operator (used in key name) 117 | j = -1 118 | lastOperator = "Unknown" 119 | while True: 120 | j = j + 1 # start with 0 121 | try: 122 | plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) 123 | except WindowsError: 124 | break 125 | except FileNotFoundError: 126 | break 127 | ktype = winreg.QueryValueEx(plkkey, None)[0] 128 | if ktype == 'operatorURL': 129 | operatorURL = winreg.QueryValueEx(plkkey, 'value')[0] 130 | try: 131 | lastOperator = operatorURL.split('//')[1].split('/')[0] 132 | except: 133 | pass 134 | 135 | 136 | # Find hashes 137 | j = -1 138 | while True: 139 | j = j + 1 # start with 0 140 | try: 141 | plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) 142 | except WindowsError: 143 | break 144 | except FileNotFoundError: 145 | break 146 | ktype = winreg.QueryValueEx(plkkey, None)[0] 147 | 148 | if ktype == "passHash": 149 | passhash_encrypted = winreg.QueryValueEx(plkkey, 'value')[0] 150 | names.append("ADE_key_" + lastOperator + "_" + str(int(time.time())) + "_" + str(idx)) 151 | idx = idx + 1 152 | keys.append(passhash_encrypted) 153 | 154 | if fp is None: 155 | #print("Didn't find fingerprint for decryption ...") 156 | return [], [] 157 | 158 | print("Found {0:d} passhashes".format(len(keys)), file=sys.stderr) 159 | 160 | keys_decrypted = [] 161 | 162 | for key in keys: 163 | decrypted = decrypt_passhash(key, fp) 164 | #print("Input key: " + key) 165 | #print("Output key: " + decrypted) 166 | keys_decrypted.append(decrypted) 167 | 168 | return keys_decrypted, names 169 | 170 | 171 | else: 172 | def passhash_keys(): 173 | raise ADEPTError("This script only supports Windows.") 174 | #TODO: Add MacOS support by parsing the activation.xml file. 175 | return [], [] 176 | 177 | 178 | if __name__ == '__main__': 179 | print("This is a python calibre plugin. It can't be directly executed.") 180 | -------------------------------------------------------------------------------- /DeDRM_plugin/alfcrypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # crypto library mainly by some_updates 5 | 6 | # pbkdf2.py pbkdf2 code taken from pbkdf2.py 7 | # pbkdf2.py Copyright © 2004 Matt Johnston 8 | # pbkdf2.py Copyright © 2009 Daniel Holth 9 | # pbkdf2.py This code may be freely used and modified for any purpose. 10 | 11 | import sys 12 | import hmac 13 | from struct import pack 14 | import hashlib 15 | import aescbc 16 | 17 | class Pukall_Cipher(object): 18 | def __init__(self): 19 | self.key = None 20 | 21 | def PC1(self, key, src, decryption=True): 22 | sum1 = 0; 23 | sum2 = 0; 24 | keyXorVal = 0; 25 | if len(key)!=16: 26 | raise Exception("PC1: Bad key length") 27 | wkey = [] 28 | for i in range(8): 29 | if sys.version_info[0] == 2: 30 | wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) 31 | else: 32 | wkey.append(key[i*2]<<8 | key[i*2+1]) 33 | dst = bytearray(len(src)) 34 | for i in range(len(src)): 35 | temp1 = 0; 36 | byteXorVal = 0; 37 | for j in range(8): 38 | temp1 ^= wkey[j] 39 | sum2 = (sum2+j)*20021 + sum1 40 | sum1 = (temp1*346)&0xFFFF 41 | sum2 = (sum2+sum1)&0xFFFF 42 | temp1 = (temp1*20021+1)&0xFFFF 43 | byteXorVal ^= temp1 ^ sum2 44 | 45 | if sys.version_info[0] == 2: 46 | curByte = ord(src[i]) 47 | else: 48 | curByte = src[i] 49 | 50 | if not decryption: 51 | keyXorVal = curByte * 257; 52 | curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF 53 | if decryption: 54 | keyXorVal = curByte * 257; 55 | for j in range(8): 56 | wkey[j] ^= keyXorVal; 57 | 58 | if sys.version_info[0] == 2: 59 | dst[i] = chr(curByte) 60 | else: 61 | dst[i] = curByte 62 | 63 | return bytes(dst) 64 | 65 | class Topaz_Cipher(object): 66 | def __init__(self): 67 | self._ctx = None 68 | 69 | def ctx_init(self, key): 70 | ctx1 = 0x0CAFFE19E 71 | if isinstance(key, str): 72 | key = key.encode('latin-1') 73 | for keyByte in key: 74 | ctx2 = ctx1 75 | ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF ) 76 | self._ctx = [ctx1, ctx2] 77 | return [ctx1,ctx2] 78 | 79 | def decrypt(self, data, ctx=None): 80 | if ctx == None: 81 | ctx = self._ctx 82 | ctx1 = ctx[0] 83 | ctx2 = ctx[1] 84 | plainText = "" 85 | if isinstance(data, str): 86 | data = data.encode('latin-1') 87 | for dataByte in data: 88 | m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF 89 | ctx2 = ctx1 90 | ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF) 91 | plainText += chr(m) 92 | return plainText 93 | 94 | class AES_CBC(object): 95 | def __init__(self): 96 | self._key = None 97 | self._iv = None 98 | self.aes = None 99 | 100 | def set_decrypt_key(self, userkey, iv): 101 | self._key = userkey 102 | self._iv = iv 103 | self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey)) 104 | 105 | def decrypt(self, data): 106 | iv = self._iv 107 | cleartext = self.aes.decrypt(iv + data) 108 | return cleartext 109 | 110 | 111 | class KeyIVGen(object): 112 | # this only exists in openssl so we will use pure python implementation instead 113 | # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1', 114 | # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) 115 | def pbkdf2(self, passwd, salt, iter, keylen): 116 | 117 | def xorbytes( a, b ): 118 | if len(a) != len(b): 119 | raise Exception("xorbytes(): lengths differ") 120 | return bytes(bytearray([x ^ y for x, y in zip(a, b)])) 121 | 122 | def prf( h, data ): 123 | hm = h.copy() 124 | hm.update( data ) 125 | return hm.digest() 126 | 127 | def pbkdf2_F( h, salt, itercount, blocknum ): 128 | U = prf( h, salt + pack('>i',blocknum ) ) 129 | T = U 130 | for i in range(2, itercount+1): 131 | U = prf( h, U ) 132 | T = xorbytes( T, U ) 133 | return T 134 | 135 | sha = hashlib.sha1 136 | digest_size = sha().digest_size 137 | # l - number of output blocks to produce 138 | l = keylen // digest_size 139 | if keylen % digest_size != 0: 140 | l += 1 141 | h = hmac.new( passwd, None, sha ) 142 | T = b"" 143 | for i in range(1, l+1): 144 | T += pbkdf2_F( h, salt, iter, i ) 145 | return T[0: keylen] 146 | 147 | 148 | -------------------------------------------------------------------------------- /DeDRM_plugin/argv_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | # get sys.argv arguments and encode them into utf-8 7 | def unicode_argv(default_name): 8 | 9 | try: 10 | from calibre.constants import iswindows 11 | except: 12 | iswindows = sys.platform.startswith('win') 13 | 14 | if iswindows: 15 | # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode 16 | # strings. 17 | 18 | # Versions 2.x of Python don't support Unicode in sys.argv on 19 | # Windows, with the underlying Windows API instead replacing multi-byte 20 | # characters with '?'. 21 | 22 | 23 | from ctypes import POINTER, byref, cdll, c_int, windll 24 | from ctypes.wintypes import LPCWSTR, LPWSTR 25 | 26 | GetCommandLineW = cdll.kernel32.GetCommandLineW 27 | GetCommandLineW.argtypes = [] 28 | GetCommandLineW.restype = LPCWSTR 29 | 30 | CommandLineToArgvW = windll.shell32.CommandLineToArgvW 31 | CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] 32 | CommandLineToArgvW.restype = POINTER(LPWSTR) 33 | 34 | cmd = GetCommandLineW() 35 | argc = c_int(0) 36 | argv = CommandLineToArgvW(cmd, byref(argc)) 37 | if argc.value > 0: 38 | # Remove Python executable and commands if present 39 | start = argc.value - len(sys.argv) 40 | return [argv[i] for i in 41 | range(start, argc.value)] 42 | # if we don't have any arguments at all, just pass back script name 43 | # this should never happen 44 | return [ default_name ] 45 | else: 46 | argvencoding = sys.stdin.encoding or "utf-8" 47 | return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv] 48 | 49 | -------------------------------------------------------------------------------- /DeDRM_plugin/epubtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is a python script. You need a Python interpreter to run it. 5 | # For example, ActiveState Python, which exists for windows. 6 | # 7 | # Changelog drmcheck 8 | # 1.00 - Initial version, with code from various other scripts 9 | # 1.01 - Moved authorship announcement to usage section. 10 | # 11 | # Changelog epubtest 12 | # 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf 13 | # 1.01 - Added routine for use by Windows DeDRM 14 | # 2.00 - Python 3, September 2020 15 | # 2.01 - Add new Adobe DRM, add Readium LCP 16 | # 17 | # Written in 2011 by Paul Durrant 18 | # Released with unlicense. See http://unlicense.org/ 19 | # 20 | ############################################################################# 21 | # 22 | # This is free and unencumbered software released into the public domain. 23 | # 24 | # Anyone is free to copy, modify, publish, use, compile, sell, or 25 | # distribute this software, either in source code form or as a compiled 26 | # binary, for any purpose, commercial or non-commercial, and by any 27 | # means. 28 | # 29 | # In jurisdictions that recognize copyright laws, the author or authors 30 | # of this software dedicate any and all copyright interest in the 31 | # software to the public domain. We make this dedication for the benefit 32 | # of the public at large and to the detriment of our heirs and 33 | # successors. We intend this dedication to be an overt act of 34 | # relinquishment in perpetuity of all present and future rights to this 35 | # software under copyright law. 36 | # 37 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 38 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 39 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 40 | # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 41 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 42 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 43 | # OTHER DEALINGS IN THE SOFTWARE. 44 | # 45 | ############################################################################# 46 | # 47 | # It's still polite to give attribution if you do reuse this code. 48 | # 49 | 50 | __version__ = '2.0' 51 | 52 | #@@CALIBRE_COMPAT_CODE@@ 53 | 54 | import sys, struct, os, traceback 55 | import zlib 56 | import zipfile 57 | import xml.etree.ElementTree as etree 58 | from .argv_utils import unicode_argv 59 | 60 | NSMAP = {'adept': 'http://ns.adobe.com/adept', 61 | 'enc': 'http://www.w3.org/2001/04/xmlenc#'} 62 | 63 | from .utilities import SafeUnbuffered 64 | 65 | 66 | _FILENAME_LEN_OFFSET = 26 67 | _EXTRA_LEN_OFFSET = 28 68 | _FILENAME_OFFSET = 30 69 | _MAX_SIZE = 64 * 1024 70 | 71 | 72 | def uncompress(cmpdata): 73 | dc = zlib.decompressobj(-15) 74 | data = '' 75 | while len(cmpdata) > 0: 76 | if len(cmpdata) > _MAX_SIZE : 77 | newdata = cmpdata[0:_MAX_SIZE] 78 | cmpdata = cmpdata[_MAX_SIZE:] 79 | else: 80 | newdata = cmpdata 81 | cmpdata = '' 82 | newdata = dc.decompress(newdata) 83 | unprocessed = dc.unconsumed_tail 84 | if len(unprocessed) == 0: 85 | newdata += dc.flush() 86 | data += newdata 87 | cmpdata += unprocessed 88 | unprocessed = '' 89 | return data 90 | 91 | def getfiledata(file, zi): 92 | # get file name length and exta data length to find start of file data 93 | local_header_offset = zi.header_offset 94 | 95 | file.seek(local_header_offset + _FILENAME_LEN_OFFSET) 96 | leninfo = file.read(2) 97 | local_name_length, = struct.unpack('" in inzip.read("META-INF/rights.xml"): 138 | # Untested, just found this info on Google 139 | encryption = "Kobo" 140 | 141 | elif 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist: 142 | encryption = "Unencrypted" 143 | else: 144 | try: 145 | rights = etree.fromstring(inzip.read('META-INF/rights.xml')) 146 | adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) 147 | expr = './/%s' % (adept('encryptedKey'),) 148 | bookkey = ''.join(rights.findtext(expr)) 149 | if len(bookkey) >= 172: 150 | encryption = "Adobe" 151 | elif len(bookkey) == 64: 152 | encryption = "B&N" 153 | else: 154 | encryption = "Unknown (key len " + str(len(bookkey)) + ")" 155 | except: 156 | encryption = "Unknown" 157 | except: 158 | traceback.print_exc() 159 | return encryption 160 | 161 | def main(): 162 | argv=unicode_argv("epubtest.py") 163 | if len(argv) < 2: 164 | print("Give an ePub file as a parameter.") 165 | else: 166 | print(encryption(argv[1])) 167 | return 0 168 | 169 | if __name__ == "__main__": 170 | sys.stdout=SafeUnbuffered(sys.stdout) 171 | sys.stderr=SafeUnbuffered(sys.stderr) 172 | sys.exit(main()) 173 | -------------------------------------------------------------------------------- /DeDRM_plugin/ignoblekeyAndroid.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Extracts the user's ccHash from an .adobe-digital-editions folder 3 | typically included in the Nook Android app's data folder. 4 | 5 | Based on ignoblekeyWindowsStore.py, updated for Android by noDRM. 6 | ''' 7 | 8 | import sys 9 | import os 10 | import base64 11 | try: 12 | from Cryptodome.Cipher import AES 13 | except ImportError: 14 | from Crypto.Cipher import AES 15 | import hashlib 16 | from lxml import etree 17 | 18 | def unpad(data, padding=16): 19 | if sys.version_info[0] == 2: 20 | pad_len = ord(data[-1]) 21 | else: 22 | pad_len = data[-1] 23 | 24 | return data[:-pad_len] 25 | 26 | 27 | PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" 28 | 29 | 30 | def dump_keys(path_to_adobe_folder): 31 | 32 | activation_path = os.path.join(path_to_adobe_folder, "activation.xml") 33 | device_path = os.path.join(path_to_adobe_folder, "device.xml") 34 | 35 | if not os.path.isfile(activation_path): 36 | print("Nook activation file is missing: %s\n" % activation_path) 37 | return [] 38 | if not os.path.isfile(device_path): 39 | print("Nook device file is missing: %s\n" % device_path) 40 | return [] 41 | 42 | # Load files: 43 | activation_xml = etree.parse(activation_path) 44 | device_xml = etree.parse(device_path) 45 | 46 | # Get fingerprint: 47 | device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text 48 | device_fingerprint = base64.b64decode(device_fingerprint).hex() 49 | 50 | hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16] 51 | 52 | hashes = [] 53 | 54 | for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"): 55 | try: 56 | encrypted_cc_hash = base64.b64decode(pass_hash.text) 57 | cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:])) 58 | hashes.append(base64.b64encode(cc_hash).decode("ascii")) 59 | #print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii"))) 60 | except: 61 | pass 62 | 63 | return hashes 64 | 65 | 66 | 67 | if __name__ == "__main__": 68 | print("No standalone version available.") 69 | -------------------------------------------------------------------------------- /DeDRM_plugin/ignoblekeyGenPassHash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ignoblekeyGenPassHash.py 5 | # Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al. 6 | 7 | # Released under the terms of the GNU General Public Licence, version 3 8 | # 9 | 10 | # Windows users: Before running this program, you must first install Python. 11 | # We recommend ActiveState Python 2.7.X for Windows (x86) from 12 | # http://www.activestate.com/activepython/downloads. 13 | # You must also install PyCrypto from 14 | # http://www.voidspace.org.uk/python/modules.shtml#pycrypto 15 | # (make certain to install the version for Python 2.7). 16 | # Then save this script file as ignoblekeygen.pyw and double-click on it to run it. 17 | # 18 | # Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this 19 | # program from the command line (python ignoblekeygen.pyw) or by double-clicking 20 | # it when it has been associated with PythonLauncher. 21 | 22 | # Revision history: 23 | # 1 - Initial release 24 | # 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) 25 | # 2.1 - Allow Windows versions of libcrypto to be found 26 | # 2.2 - On Windows try PyCrypto first and then OpenSSL next 27 | # 2.3 - Modify interface to allow use of import 28 | # 2.4 - Improvements to UI and now works in plugins 29 | # 2.5 - Additional improvement for unicode and plugin support 30 | # 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility 31 | # 2.7 - Work if TkInter is missing 32 | # 2.8 - Fix bug in stand-alone use (import tkFileDialog) 33 | # 3.0 - Added Python 3 compatibility for calibre 5.0 34 | # 3.1 - Remove OpenSSL support, only PyCryptodome is supported now 35 | 36 | """ 37 | Generate Barnes & Noble EPUB user key from name and credit card number. 38 | """ 39 | 40 | __license__ = 'GPL v3' 41 | __version__ = "3.1" 42 | 43 | import sys 44 | import os 45 | import hashlib 46 | import base64 47 | 48 | #@@CALIBRE_COMPAT_CODE@@ 49 | 50 | try: 51 | from Cryptodome.Cipher import AES 52 | except ImportError: 53 | from Crypto.Cipher import AES 54 | 55 | from .utilities import SafeUnbuffered 56 | 57 | from .argv_utils import unicode_argv 58 | 59 | class IGNOBLEError(Exception): 60 | pass 61 | 62 | def normalize_name(name): 63 | return ''.join(x for x in name.lower() if x != ' ') 64 | 65 | 66 | def generate_key(name, ccn): 67 | # remove spaces and case from name and CC numbers. 68 | name = normalize_name(name) 69 | ccn = normalize_name(ccn) 70 | 71 | if type(name)==str: 72 | name = name.encode('utf-8') 73 | if type(ccn)==str: 74 | ccn = ccn.encode('utf-8') 75 | 76 | name = name + b'\x00' 77 | ccn = ccn + b'\x00' 78 | 79 | name_sha = hashlib.sha1(name).digest()[:16] 80 | ccn_sha = hashlib.sha1(ccn).digest()[:16] 81 | both_sha = hashlib.sha1(name + ccn).digest() 82 | crypt = AES.new(ccn_sha, AES.MODE_CBC, name_sha).encrypt(both_sha + (b'\x0c' * 0x0c)) 83 | userkey = hashlib.sha1(crypt).digest() 84 | return base64.b64encode(userkey) 85 | 86 | 87 | def cli_main(): 88 | sys.stdout=SafeUnbuffered(sys.stdout) 89 | sys.stderr=SafeUnbuffered(sys.stderr) 90 | argv=unicode_argv("ignoblekeyGenPassHash.py") 91 | progname = os.path.basename(argv[0]) 92 | if len(argv) != 4: 93 | print("usage: {0} ".format(progname)) 94 | return 1 95 | name, ccn, keypath = argv[1:] 96 | userkey = generate_key(name, ccn) 97 | open(keypath,'wb').write(userkey) 98 | return 0 99 | 100 | 101 | def gui_main(): 102 | try: 103 | import tkinter 104 | import tkinter.constants 105 | import tkinter.messagebox 106 | import tkinter.filedialog 107 | import traceback 108 | except: 109 | return cli_main() 110 | 111 | class DecryptionDialog(tkinter.Frame): 112 | def __init__(self, root): 113 | tkinter.Frame.__init__(self, root, border=5) 114 | self.status = tkinter.Label(self, text="Enter parameters") 115 | self.status.pack(fill=tkinter.constants.X, expand=1) 116 | body = tkinter.Frame(self) 117 | body.pack(fill=tkinter.constants.X, expand=1) 118 | sticky = tkinter.constants.E + tkinter.constants.W 119 | body.grid_columnconfigure(1, weight=2) 120 | tkinter.Label(body, text="Account Name").grid(row=0) 121 | self.name = tkinter.Entry(body, width=40) 122 | self.name.grid(row=0, column=1, sticky=sticky) 123 | tkinter.Label(body, text="CC#").grid(row=1) 124 | self.ccn = tkinter.Entry(body, width=40) 125 | self.ccn.grid(row=1, column=1, sticky=sticky) 126 | tkinter.Label(body, text="Output file").grid(row=2) 127 | self.keypath = tkinter.Entry(body, width=40) 128 | self.keypath.grid(row=2, column=1, sticky=sticky) 129 | self.keypath.insert(2, "bnepubkey.b64") 130 | button = tkinter.Button(body, text="...", command=self.get_keypath) 131 | button.grid(row=2, column=2) 132 | buttons = tkinter.Frame(self) 133 | buttons.pack() 134 | botton = tkinter.Button( 135 | buttons, text="Generate", width=10, command=self.generate) 136 | botton.pack(side=tkinter.constants.LEFT) 137 | tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) 138 | button = tkinter.Button( 139 | buttons, text="Quit", width=10, command=self.quit) 140 | button.pack(side=tkinter.constants.RIGHT) 141 | 142 | def get_keypath(self): 143 | keypath = tkinter.filedialog.asksaveasfilename( 144 | parent=None, title="Select B&N ePub key file to produce", 145 | defaultextension=".b64", 146 | filetypes=[('base64-encoded files', '.b64'), 147 | ('All Files', '.*')]) 148 | if keypath: 149 | keypath = os.path.normpath(keypath) 150 | self.keypath.delete(0, tkinter.constants.END) 151 | self.keypath.insert(0, keypath) 152 | return 153 | 154 | def generate(self): 155 | name = self.name.get() 156 | ccn = self.ccn.get() 157 | keypath = self.keypath.get() 158 | if not name: 159 | self.status['text'] = "Name not specified" 160 | return 161 | if not ccn: 162 | self.status['text'] = "Credit card number not specified" 163 | return 164 | if not keypath: 165 | self.status['text'] = "Output keyfile path not specified" 166 | return 167 | self.status['text'] = "Generating..." 168 | try: 169 | userkey = generate_key(name, ccn) 170 | except Exception as e: 171 | self.status['text'] = "Error: (0}".format(e.args[0]) 172 | return 173 | open(keypath,'wb').write(userkey) 174 | self.status['text'] = "Keyfile successfully generated" 175 | 176 | root = tkinter.Tk() 177 | root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) 178 | root.resizable(True, False) 179 | root.minsize(300, 0) 180 | DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) 181 | root.mainloop() 182 | return 0 183 | 184 | if __name__ == '__main__': 185 | if len(sys.argv) > 1: 186 | sys.exit(cli_main()) 187 | sys.exit(gui_main()) 188 | -------------------------------------------------------------------------------- /DeDRM_plugin/ignoblekeyWindowsStore.py: -------------------------------------------------------------------------------- 1 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 2 | 3 | ''' 4 | Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app. 5 | https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h 6 | (Requires a recent Windows version in a supported region (US).) 7 | This procedure has been tested with Nook app version 1.11.0.4 under Windows 11. 8 | 9 | Based on experimental standalone python script created by fesiwi at 10 | https://github.com/noDRM/DeDRM_tools/discussions/9 11 | ''' 12 | 13 | import sys, os 14 | import apsw 15 | import base64 16 | import traceback 17 | try: 18 | from Cryptodome.Cipher import AES 19 | except: 20 | from Crypto.Cipher import AES 21 | import hashlib 22 | from lxml import etree 23 | 24 | def unpad(data, padding=16): 25 | if sys.version_info[0] == 2: 26 | pad_len = ord(data[-1]) 27 | else: 28 | pad_len = data[-1] 29 | 30 | return data[:-pad_len] 31 | 32 | 33 | NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState" 34 | PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e" 35 | 36 | 37 | def dump_keys(print_result=False): 38 | db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3") 39 | 40 | 41 | if not os.path.isfile(db_filename): 42 | print("Database file not found. Is the Nook Windows Store app installed?") 43 | return [] 44 | 45 | 46 | # Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result. 47 | # There should only be one result anyways. 48 | serial_number = apsw.Connection(db_filename).cursor().execute( 49 | "SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0] 50 | 51 | 52 | hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16] 53 | 54 | activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml") 55 | 56 | if not os.path.isfile(activation_file_name): 57 | print("Activation file not found. Are you logged in to your Nook account?") 58 | return [] 59 | 60 | 61 | activation_xml = etree.parse(activation_file_name) 62 | 63 | decrypted_hashes = [] 64 | 65 | for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"): 66 | try: 67 | encrypted_cc_hash = base64.b64decode(pass_hash.text) 68 | cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16) 69 | decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii"))) 70 | if print_result: 71 | print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii"))) 72 | except: 73 | traceback.print_exc() 74 | 75 | return decrypted_hashes 76 | 77 | if __name__ == "__main__": 78 | dump_keys(True) 79 | -------------------------------------------------------------------------------- /DeDRM_plugin/kfxdedrm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Engine to remove drm from Kindle KFX ebooks 5 | 6 | # 2.0 - Python 3 for calibre 5.0 7 | # 2.1 - Some fixes for debugging 8 | # 2.1.1 - Whitespace! 9 | 10 | 11 | import os, sys 12 | import shutil 13 | import traceback 14 | import zipfile 15 | 16 | from io import BytesIO 17 | 18 | 19 | #@@CALIBRE_COMPAT_CODE@@ 20 | 21 | 22 | from ion import DrmIon, DrmIonVoucher 23 | 24 | 25 | 26 | __license__ = 'GPL v3' 27 | __version__ = '2.0' 28 | 29 | 30 | class KFXZipBook: 31 | def __init__(self, infile): 32 | self.infile = infile 33 | self.voucher = None 34 | self.decrypted = {} 35 | 36 | def getPIDMetaInfo(self): 37 | return (None, None) 38 | 39 | def processBook(self, totalpids): 40 | with zipfile.ZipFile(self.infile, 'r') as zf: 41 | for filename in zf.namelist(): 42 | with zf.open(filename) as fh: 43 | data = fh.read(8) 44 | if data != b'\xeaDRMION\xee': 45 | continue 46 | data += fh.read() 47 | if self.voucher is None: 48 | self.decrypt_voucher(totalpids) 49 | print("Decrypting KFX DRMION: {0}".format(filename)) 50 | outfile = BytesIO() 51 | DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile) 52 | self.decrypted[filename] = outfile.getvalue() 53 | 54 | if not self.decrypted: 55 | print("The .kfx-zip archive does not contain an encrypted DRMION file") 56 | 57 | def decrypt_voucher(self, totalpids): 58 | with zipfile.ZipFile(self.infile, 'r') as zf: 59 | for info in zf.infolist(): 60 | with zf.open(info.filename) as fh: 61 | data = fh.read(4) 62 | if data != b'\xe0\x01\x00\xea': 63 | continue 64 | 65 | data += fh.read() 66 | if b'ProtectedData' in data: 67 | break # found DRM voucher 68 | else: 69 | raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher") 70 | 71 | print("Decrypting KFX DRM voucher: {0}".format(info.filename)) 72 | 73 | for pid in [''] + totalpids: 74 | # Belt and braces. PIDs should be unicode strings, but just in case... 75 | if isinstance(pid, bytes): 76 | pid = pid.decode('ascii') 77 | for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,0), (32,40), (40,0), (40,40)]: 78 | if len(pid) == dsn_len + secret_len: 79 | break # split pid into DSN and account secret 80 | else: 81 | continue 82 | 83 | try: 84 | voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:]) 85 | voucher.parse() 86 | voucher.decryptvoucher() 87 | break 88 | except: 89 | traceback.print_exc() 90 | pass 91 | else: 92 | raise Exception("Failed to decrypt KFX DRM voucher with any key") 93 | 94 | print("KFX DRM voucher successfully decrypted") 95 | 96 | license_type = voucher.getlicensetype() 97 | if license_type != "Purchase": 98 | #raise Exception(("This book is licensed as {0}. " 99 | # 'These tools are intended for use on purchased books.').format(license_type)) 100 | print("Warning: This book is licensed as {0}. " 101 | "These tools are intended for use on purchased books. Continuing ...".format(license_type)) 102 | 103 | self.voucher = voucher 104 | 105 | def getBookTitle(self): 106 | return os.path.splitext(os.path.split(self.infile)[1])[0] 107 | 108 | def getBookExtension(self): 109 | return '.kfx-zip' 110 | 111 | def getBookType(self): 112 | return 'KFX-ZIP' 113 | 114 | def cleanup(self): 115 | pass 116 | 117 | def getFile(self, outpath): 118 | if not self.decrypted: 119 | shutil.copyfile(self.infile, outpath) 120 | else: 121 | with zipfile.ZipFile(self.infile, 'r') as zif: 122 | with zipfile.ZipFile(outpath, 'w') as zof: 123 | for info in zif.infolist(): 124 | zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename))) 125 | -------------------------------------------------------------------------------- /DeDRM_plugin/kindlepid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Mobipocket PID calculator v0.4 for Amazon Kindle. 5 | # Copyright (c) 2007, 2009 Igor Skochinsky 6 | # History: 7 | # 0.1 Initial release 8 | # 0.2 Added support for generating PID for iPhone (thanks to mbp) 9 | # 0.3 changed to autoflush stdout, fixed return code usage 10 | # 0.3 updated for unicode 11 | # 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. 12 | # 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility 13 | # 1.0 Python 3 for calibre 5.0 14 | 15 | 16 | import sys 17 | import binascii 18 | 19 | #@@CALIBRE_COMPAT_CODE@@ 20 | 21 | from .utilities import SafeUnbuffered 22 | from .argv_utils import unicode_argv 23 | 24 | letters = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' 25 | 26 | def crc32(s): 27 | return (~binascii.crc32(s,-1))&0xFFFFFFFF 28 | 29 | def checksumPid(s): 30 | crc = crc32(s) 31 | crc = crc ^ (crc >> 16) 32 | res = s 33 | l = len(letters) 34 | for i in (0,1): 35 | b = crc & 0xff 36 | pos = (b // l) ^ (b % l) 37 | res += bytes(bytearray([letters[pos%l]])) 38 | crc >>= 8 39 | 40 | return res 41 | 42 | def pidFromSerial(s, l): 43 | crc = crc32(s) 44 | 45 | arr1 = [0]*l 46 | for i in range(len(s)): 47 | if sys.version_info[0] == 2: 48 | arr1[i%l] ^= ord(s[i]) 49 | else: 50 | arr1[i%l] ^= s[i] 51 | 52 | crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] 53 | for i in range(l): 54 | arr1[i] ^= crc_bytes[i&3] 55 | 56 | pid = b"" 57 | for i in range(l): 58 | b = arr1[i] & 0xff 59 | pid+=bytes(bytearray([letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]])) 60 | 61 | return pid 62 | 63 | def cli_main(): 64 | print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky") 65 | argv=unicode_argv("kindlepid.py") 66 | if len(argv)==2: 67 | serial = argv[1] 68 | else: 69 | print("Usage: kindlepid.py /") 70 | return 1 71 | if len(serial)==16: 72 | if serial.startswith("B") or serial.startswith("9"): 73 | print("Kindle serial number detected") 74 | else: 75 | print("Warning: unrecognized serial number. Please recheck input.") 76 | return 1 77 | pid = pidFromSerial(serial.encode("utf-8"),7)+'*' 78 | print("Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid))) 79 | return 0 80 | elif len(serial)==40: 81 | print("iPhone serial number (UDID) detected") 82 | pid = pidFromSerial(serial.encode("utf-8"),8) 83 | print("Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid))) 84 | return 0 85 | print("Warning: unrecognized serial number. Please recheck input.") 86 | return 1 87 | 88 | 89 | if __name__ == "__main__": 90 | sys.stdout=SafeUnbuffered(sys.stdout) 91 | sys.stderr=SafeUnbuffered(sys.stderr) 92 | sys.exit(cli_main()) 93 | -------------------------------------------------------------------------------- /DeDRM_plugin/lcpdedrm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # lcpdedrm.py 5 | # Copyright © 2021-2022 NoDRM 6 | 7 | # Released under the terms of the GNU General Public Licence, version 3 8 | # 9 | 10 | 11 | # Revision history: 12 | # 1 - Initial release 13 | # 2 - LCP DRM code removed due to a DMCA takedown. 14 | 15 | """ 16 | This file used to contain code to remove the Readium LCP DRM 17 | from eBooks. Unfortunately, Readium has issued a DMCA takedown 18 | request, so I was forced to remove that code: 19 | 20 | https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md 21 | 22 | This file now just returns an error message when asked to remove LCP DRM. 23 | For more information, see this issue: 24 | https://github.com/noDRM/DeDRM_tools/issues/18 25 | """ 26 | 27 | __license__ = 'GPL v3' 28 | __version__ = "2" 29 | 30 | import json 31 | from zipfile import ZipFile 32 | from contextlib import closing 33 | 34 | 35 | class LCPError(Exception): 36 | pass 37 | 38 | # Check file to see if this is an LCP-protected file 39 | def isLCPbook(inpath): 40 | try: 41 | with closing(ZipFile(open(inpath, 'rb'))) as lcpbook: 42 | if ("META-INF/license.lcpl" not in lcpbook.namelist() or 43 | "META-INF/encryption.xml" not in lcpbook.namelist() or 44 | b"EncryptedContentKey" not in lcpbook.read("META-INF/encryption.xml")): 45 | return False 46 | 47 | license = json.loads(lcpbook.read('META-INF/license.lcpl')) 48 | 49 | if "id" in license and "encryption" in license and "profile" in license["encryption"]: 50 | return True 51 | 52 | except: 53 | return False 54 | 55 | return False 56 | 57 | 58 | # Takes a file and a list of passphrases 59 | def decryptLCPbook(inpath, passphrases, parent_object): 60 | 61 | if not isLCPbook(inpath): 62 | raise LCPError("This is not an LCP-encrypted book") 63 | 64 | print("LCP: LCP DRM removal no longer supported due to a DMCA takedown request.") 65 | print("LCP: The takedown request can be found here: ") 66 | print("LCP: https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md ") 67 | print("LCP: More information can be found in the Github repository: ") 68 | print("LCP: https://github.com/noDRM/DeDRM_tools/issues/18 ") 69 | 70 | raise LCPError("LCP DRM removal no longer supported") 71 | -------------------------------------------------------------------------------- /DeDRM_plugin/plugin-import-name-dedrm.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/DeDRM_plugin/plugin-import-name-dedrm.txt -------------------------------------------------------------------------------- /DeDRM_plugin/prefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 4 | 5 | __license__ = 'GPL v3' 6 | 7 | # Standard Python modules. 8 | import os, sys 9 | import traceback 10 | 11 | 12 | #@@CALIBRE_COMPAT_CODE@@ 13 | 14 | 15 | try: 16 | from calibre.utils.config import JSONConfig 17 | except: 18 | from standalone.jsonconfig import JSONConfig 19 | 20 | from __init__ import PLUGIN_NAME 21 | 22 | class DeDRM_Prefs(): 23 | def __init__(self, json_path=None): 24 | if json_path is None: 25 | JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') 26 | else: 27 | JSON_PATH = json_path 28 | 29 | self.dedrmprefs = JSONConfig(JSON_PATH) 30 | 31 | self.dedrmprefs.defaults['configured'] = False 32 | self.dedrmprefs.defaults['deobfuscate_fonts'] = True 33 | self.dedrmprefs.defaults['remove_watermarks'] = False 34 | self.dedrmprefs.defaults['bandnkeys'] = {} 35 | self.dedrmprefs.defaults['adeptkeys'] = {} 36 | self.dedrmprefs.defaults['ereaderkeys'] = {} 37 | self.dedrmprefs.defaults['kindlekeys'] = {} 38 | self.dedrmprefs.defaults['androidkeys'] = {} 39 | self.dedrmprefs.defaults['pids'] = [] 40 | self.dedrmprefs.defaults['serials'] = [] 41 | self.dedrmprefs.defaults['lcp_passphrases'] = [] 42 | self.dedrmprefs.defaults['adobe_pdf_passphrases'] = [] 43 | self.dedrmprefs.defaults['adobewineprefix'] = "" 44 | self.dedrmprefs.defaults['kindlewineprefix'] = "" 45 | 46 | # initialise 47 | # we must actually set the prefs that are dictionaries and lists 48 | # to empty dictionaries and lists, otherwise we are unable to add to them 49 | # as then it just adds to the (memory only) dedrmprefs.defaults versions! 50 | if self.dedrmprefs['bandnkeys'] == {}: 51 | self.dedrmprefs['bandnkeys'] = {} 52 | if self.dedrmprefs['adeptkeys'] == {}: 53 | self.dedrmprefs['adeptkeys'] = {} 54 | if self.dedrmprefs['ereaderkeys'] == {}: 55 | self.dedrmprefs['ereaderkeys'] = {} 56 | if self.dedrmprefs['kindlekeys'] == {}: 57 | self.dedrmprefs['kindlekeys'] = {} 58 | if self.dedrmprefs['androidkeys'] == {}: 59 | self.dedrmprefs['androidkeys'] = {} 60 | if self.dedrmprefs['pids'] == []: 61 | self.dedrmprefs['pids'] = [] 62 | if self.dedrmprefs['serials'] == []: 63 | self.dedrmprefs['serials'] = [] 64 | if self.dedrmprefs['lcp_passphrases'] == []: 65 | self.dedrmprefs['lcp_passphrases'] = [] 66 | if self.dedrmprefs['adobe_pdf_passphrases'] == []: 67 | self.dedrmprefs['adobe_pdf_passphrases'] = [] 68 | 69 | def __getitem__(self,kind = None): 70 | if kind is not None: 71 | return self.dedrmprefs[kind] 72 | return self.dedrmprefs 73 | 74 | def set(self, kind, value): 75 | self.dedrmprefs[kind] = value 76 | 77 | def writeprefs(self,value = True): 78 | self.dedrmprefs['configured'] = value 79 | 80 | def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue): 81 | try: 82 | if keyvalue not in self.dedrmprefs[prefkind].values(): 83 | # ensure that the keyname is unique 84 | # by adding a number (starting with 2) to the name if it is not 85 | namecount = 1 86 | newname = keyname 87 | while newname in self.dedrmprefs[prefkind]: 88 | namecount += 1 89 | newname = "{0:s}_{1:d}".format(keyname,namecount) 90 | # add to the preferences 91 | self.dedrmprefs[prefkind][newname] = keyvalue 92 | return (True, newname) 93 | except: 94 | traceback.print_exc() 95 | pass 96 | return (False, keyname) 97 | 98 | def addvaluetoprefs(self, prefkind, prefsvalue): 99 | # ensure the keyvalue isn't already in the preferences 100 | try: 101 | if prefsvalue not in self.dedrmprefs[prefkind]: 102 | self.dedrmprefs[prefkind].append(prefsvalue) 103 | return True 104 | except: 105 | traceback.print_exc() 106 | return False 107 | -------------------------------------------------------------------------------- /DeDRM_plugin/scriptinterface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab 4 | 5 | 6 | import sys 7 | import os 8 | 9 | 10 | #@@CALIBRE_COMPAT_CODE@@ 11 | 12 | 13 | import re 14 | import traceback 15 | import ineptepub 16 | import epubtest 17 | import zipfix 18 | import ineptpdf 19 | import erdr2pml 20 | import k4mobidedrm 21 | 22 | def decryptepub(infile, outdir, rscpath): 23 | errlog = '' 24 | 25 | # first fix the epub to make sure we do not get errors 26 | name, ext = os.path.splitext(os.path.basename(infile)) 27 | bpath = os.path.dirname(infile) 28 | zippath = os.path.join(bpath,name + '_temp.zip') 29 | rv = zipfix.repairBook(infile, zippath) 30 | if rv != 0: 31 | print("Error while trying to fix epub") 32 | return rv 33 | 34 | # determine a good name for the output file 35 | outfile = os.path.join(outdir, name + '_nodrm.epub') 36 | 37 | rv = 1 38 | # first try with the Adobe adept epub 39 | if ineptepub.adeptBook(zippath): 40 | # try with any keyfiles (*.der) in the rscpath 41 | files = os.listdir(rscpath) 42 | filefilter = re.compile("\.der$", re.IGNORECASE) 43 | files = filter(filefilter.search, files) 44 | if files: 45 | for filename in files: 46 | keypath = os.path.join(rscpath, filename) 47 | userkey = open(keypath,'rb').read() 48 | try: 49 | rv = ineptepub.decryptBook(userkey, zippath, outfile) 50 | if rv == 0: 51 | print("Decrypted Adobe ePub with key file {0}".format(filename)) 52 | break 53 | except Exception as e: 54 | errlog += traceback.format_exc() 55 | errlog += str(e) 56 | rv = 1 57 | 58 | # now try with ignoble epub 59 | # try with any keyfiles (*.b64) in the rscpath 60 | files = os.listdir(rscpath) 61 | filefilter = re.compile("\.b64$", re.IGNORECASE) 62 | files = filter(filefilter.search, files) 63 | if files: 64 | for filename in files: 65 | keypath = os.path.join(rscpath, filename) 66 | userkey = open(keypath,'r').read() 67 | #print userkey 68 | try: 69 | rv = ineptepub.decryptBook(userkey, zippath, outfile) 70 | if rv == 0: 71 | print("Decrypted B&N ePub with key file {0}".format(filename)) 72 | break 73 | except Exception as e: 74 | errlog += traceback.format_exc() 75 | errlog += str(e) 76 | rv = 1 77 | else: 78 | encryption = epubtest.encryption(zippath) 79 | if encryption == "Unencrypted": 80 | print("{0} is not DRMed.".format(name)) 81 | rv = 0 82 | else: 83 | print("{0} has an unknown encryption.".format(name)) 84 | 85 | os.remove(zippath) 86 | if rv != 0: 87 | print(errlog) 88 | return rv 89 | 90 | 91 | def decryptpdf(infile, outdir, rscpath): 92 | errlog = '' 93 | rv = 1 94 | 95 | # determine a good name for the output file 96 | name, ext = os.path.splitext(os.path.basename(infile)) 97 | outfile = os.path.join(outdir, name + '_nodrm.pdf') 98 | 99 | # try with any keyfiles (*.der) in the rscpath 100 | files = os.listdir(rscpath) 101 | filefilter = re.compile("\.der$", re.IGNORECASE) 102 | files = filter(filefilter.search, files) 103 | if files: 104 | for filename in files: 105 | keypath = os.path.join(rscpath, filename) 106 | userkey = open(keypath,'rb').read() 107 | try: 108 | rv = ineptpdf.decryptBook(userkey, infile, outfile) 109 | if rv == 0: 110 | break 111 | except Exception as e: 112 | errlog += traceback.format_exc() 113 | errlog += str(e) 114 | rv = 1 115 | 116 | if rv != 0: 117 | print(errlog) 118 | return rv 119 | 120 | 121 | def decryptpdb(infile, outdir, rscpath): 122 | errlog = '' 123 | outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz" 124 | outpath = os.path.join(outdir, outname) 125 | rv = 1 126 | socialpath = os.path.join(rscpath,'sdrmlist.txt') 127 | if os.path.exists(socialpath): 128 | keydata = open(socialpath,'r').read() 129 | keydata = keydata.rstrip(os.linesep) 130 | ar = keydata.split(',') 131 | for i in ar: 132 | try: 133 | name, cc8 = i.split(':') 134 | except ValueError: 135 | print(' Error parsing user supplied social drm data.') 136 | return 1 137 | try: 138 | rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) 139 | except Exception as e: 140 | errlog += traceback.format_exc() 141 | errlog += str(e) 142 | rv = 1 143 | 144 | if rv == 0: 145 | break 146 | return rv 147 | 148 | 149 | def decryptk4mobi(infile, outdir, rscpath): 150 | errlog = '' 151 | rv = 1 152 | pidnums = [] 153 | pidspath = os.path.join(rscpath,'pidlist.txt') 154 | if os.path.exists(pidspath): 155 | pidstr = open(pidspath,'r').read() 156 | pidstr = pidstr.rstrip(os.linesep) 157 | pidstr = pidstr.strip() 158 | if pidstr != '': 159 | pidnums = pidstr.split(',') 160 | serialnums = [] 161 | serialnumspath = os.path.join(rscpath,'seriallist.txt') 162 | if os.path.exists(serialnumspath): 163 | serialstr = open(serialnumspath,'r').read() 164 | serialstr = serialstr.rstrip(os.linesep) 165 | serialstr = serialstr.strip() 166 | if serialstr != '': 167 | serialnums = serialstr.split(',') 168 | kDatabaseFiles = [] 169 | files = os.listdir(rscpath) 170 | filefilter = re.compile("\.k4i$", re.IGNORECASE) 171 | files = filter(filefilter.search, files) 172 | if files: 173 | for filename in files: 174 | dpath = os.path.join(rscpath,filename) 175 | kDatabaseFiles.append(dpath) 176 | androidFiles = [] 177 | files = os.listdir(rscpath) 178 | filefilter = re.compile("\.ab$", re.IGNORECASE) 179 | files = filter(filefilter.search, files) 180 | if files: 181 | for filename in files: 182 | dpath = os.path.join(rscpath,filename) 183 | androidFiles.append(dpath) 184 | files = os.listdir(rscpath) 185 | filefilter = re.compile("\.db$", re.IGNORECASE) 186 | files = filter(filefilter.search, files) 187 | if files: 188 | for filename in files: 189 | dpath = os.path.join(rscpath,filename) 190 | androidFiles.append(dpath) 191 | files = os.listdir(rscpath) 192 | filefilter = re.compile("\.xml$", re.IGNORECASE) 193 | files = filter(filefilter.search, files) 194 | if files: 195 | for filename in files: 196 | dpath = os.path.join(rscpath,filename) 197 | androidFiles.append(dpath) 198 | try: 199 | rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums) 200 | except Exception as e: 201 | errlog += traceback.format_exc() 202 | errlog += str(e) 203 | rv = 1 204 | 205 | return rv 206 | -------------------------------------------------------------------------------- /DeDRM_plugin/standalone/jsonconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # CLI interface for the DeDRM plugin (useable without Calibre, too) 5 | # Config implementation 6 | 7 | from __future__ import absolute_import, print_function 8 | 9 | # Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3 10 | 11 | """ 12 | 13 | NOTE: This code is not functional (yet). I started working on it a while ago 14 | to make a standalone version of the plugins that could work without Calibre, 15 | too, but for now there's only a rough code structure and no working code yet. 16 | 17 | Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll 18 | change in the future. 19 | 20 | """ 21 | 22 | #@@CALIBRE_COMPAT_CODE@@ 23 | 24 | import sys, os, codecs, json 25 | 26 | config_dir = "/" 27 | CONFIG_DIR_MODE = 0o700 28 | iswindows = sys.platform.startswith('win') 29 | 30 | 31 | filesystem_encoding = sys.getfilesystemencoding() 32 | if filesystem_encoding is None: 33 | filesystem_encoding = 'utf-8' 34 | else: 35 | try: 36 | if codecs.lookup(filesystem_encoding).name == 'ascii': 37 | filesystem_encoding = 'utf-8' 38 | # On linux, unicode arguments to os file functions are coerced to an ascii 39 | # bytestring if sys.getfilesystemencoding() == 'ascii', which is 40 | # just plain dumb. This is fixed by the icu.py module which, when 41 | # imported changes ascii to utf-8 42 | except Exception: 43 | filesystem_encoding = 'utf-8' 44 | 45 | 46 | class JSONConfig(dict): 47 | 48 | EXTENSION = '.json' 49 | 50 | 51 | def __init__(self, rel_path_to_cf_file, base_path=config_dir): 52 | dict.__init__(self) 53 | self.no_commit = False 54 | self.defaults = {} 55 | self.file_path = os.path.join(base_path, 56 | *(rel_path_to_cf_file.split('/'))) 57 | self.file_path = os.path.abspath(self.file_path) 58 | if not self.file_path.endswith(self.EXTENSION): 59 | self.file_path += self.EXTENSION 60 | 61 | self.refresh() 62 | 63 | def mtime(self): 64 | try: 65 | return os.path.getmtime(self.file_path) 66 | except OSError: 67 | return 0 68 | 69 | def touch(self): 70 | try: 71 | os.utime(self.file_path, None) 72 | except OSError: 73 | pass 74 | 75 | 76 | def decouple(self, prefix): 77 | self.file_path = os.path.join(os.path.dirname(self.file_path), prefix + os.path.basename(self.file_path)) 78 | self.refresh() 79 | 80 | def refresh(self, clear_current=True): 81 | d = {} 82 | if os.path.exists(self.file_path): 83 | with open(self.file_path, "rb") as f: 84 | raw = f.read() 85 | try: 86 | d = self.raw_to_object(raw) if raw.strip() else {} 87 | except SystemError: 88 | pass 89 | except: 90 | import traceback 91 | traceback.print_exc() 92 | d = {} 93 | if clear_current: 94 | self.clear() 95 | self.update(d) 96 | 97 | def has_key(self, key): 98 | return dict.__contains__(self, key) 99 | 100 | def set(self, key, val): 101 | self.__setitem__(key, val) 102 | 103 | def __delitem__(self, key): 104 | try: 105 | dict.__delitem__(self, key) 106 | except KeyError: 107 | pass # ignore missing keys 108 | else: 109 | self.commit() 110 | 111 | def commit(self): 112 | if self.no_commit: 113 | return 114 | if hasattr(self, 'file_path') and self.file_path: 115 | dpath = os.path.dirname(self.file_path) 116 | if not os.path.exists(dpath): 117 | os.makedirs(dpath, mode=CONFIG_DIR_MODE) 118 | with open(self.file_path, "w") as f: 119 | raw = self.to_raw() 120 | f.seek(0) 121 | f.truncate() 122 | f.write(raw) 123 | 124 | def __enter__(self): 125 | self.no_commit = True 126 | 127 | def __exit__(self, *args): 128 | self.no_commit = False 129 | self.commit() 130 | 131 | def raw_to_object(self, raw): 132 | return json.loads(raw) 133 | 134 | def to_raw(self): 135 | return json.dumps(self, ensure_ascii=False) 136 | 137 | def __getitem__(self, key): 138 | try: 139 | return dict.__getitem__(self, key) 140 | except KeyError: 141 | return self.defaults[key] 142 | 143 | def get(self, key, default=None): 144 | try: 145 | return dict.__getitem__(self, key) 146 | except KeyError: 147 | return self.defaults.get(key, default) 148 | 149 | def __setitem__(self, key, val): 150 | dict.__setitem__(self, key, val) 151 | self.commit() -------------------------------------------------------------------------------- /DeDRM_plugin/standalone/passhash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # CLI interface for the DeDRM plugin (useable without Calibre, too) 5 | # Adobe PassHash implementation 6 | 7 | from __future__ import absolute_import, print_function 8 | 9 | # Copyright © 2021 NoDRM 10 | 11 | """ 12 | 13 | NOTE: This code is not functional (yet). I started working on it a while ago 14 | to make a standalone version of the plugins that could work without Calibre, 15 | too, but for now there's only a rough code structure and no working code yet. 16 | 17 | Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll 18 | change in the future. 19 | 20 | """ 21 | 22 | #@@CALIBRE_COMPAT_CODE@@ 23 | 24 | import os, sys 25 | 26 | from standalone.__init__ import print_opt, print_std_usage 27 | 28 | iswindows = sys.platform.startswith('win') 29 | isosx = sys.platform.startswith('darwin') 30 | 31 | def print_passhash_help(): 32 | from __version import PLUGIN_NAME, PLUGIN_VERSION 33 | print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM") 34 | print() 35 | print("passhash: Manage Adobe PassHashes") 36 | print() 37 | print_std_usage("passhash", "[ -u username -p password | -b base64str ] [ -i ] ") 38 | 39 | print() 40 | print("Options: ") 41 | print_opt("u", "username", "Generate a PassHash with the given username") 42 | print_opt("p", "password", "Generate a PassHash with the given password") 43 | print_opt("e", "extract", "Display PassHashes found on this machine") 44 | print_opt("i", "import", "Import hashes into the JSON config file") 45 | 46 | def perform_action(params, files): 47 | user = None 48 | pwd = None 49 | 50 | if len(params) == 0: 51 | print_passhash_help() 52 | return 0 53 | 54 | extract = False 55 | import_to_json = True 56 | 57 | while len(params) > 0: 58 | p = params.pop(0) 59 | if p == "--username": 60 | user = params.pop(0) 61 | elif p == "--password": 62 | pwd = params.pop(0) 63 | elif p == "--extract": 64 | extract = True 65 | elif p == "--help": 66 | print_passhash_help() 67 | return 0 68 | elif p == "--import": 69 | import_to_json = True 70 | 71 | if not extract and not import_to_json: 72 | if user is None: 73 | print("Missing parameter: --username", file=sys.stderr) 74 | if pwd is None: 75 | print("Missing parameter: --password", file=sys.stderr) 76 | if user is None or pwd is None: 77 | return 1 78 | 79 | if user is None and pwd is not None: 80 | print("Parameter --password also requires --username", file=sys.stderr) 81 | return 1 82 | if user is not None and pwd is None: 83 | print("Parameter --username also requires --password", file=sys.stderr) 84 | return 1 85 | 86 | if user is not None and pwd is not None: 87 | from ignoblekeyGenPassHash import generate_key 88 | key = generate_key(user, pwd) 89 | if import_to_json: 90 | # TODO: Import the key to the JSON 91 | pass 92 | 93 | print(key.decode("utf-8")) 94 | 95 | if extract or import_to_json: 96 | if not iswindows and not isosx: 97 | print("Extracting PassHash keys not supported on Linux.", file=sys.stderr) 98 | return 1 99 | 100 | keys = [] 101 | 102 | from ignoblekeyNookStudy import nookkeys 103 | keys.extend(nookkeys()) 104 | 105 | if iswindows: 106 | from ignoblekeyWindowsStore import dump_keys 107 | keys.extend(dump_keys()) 108 | 109 | from adobekey_get_passhash import passhash_keys 110 | ade_keys, ade_names = passhash_keys() 111 | keys.extend(ade_keys) 112 | 113 | # Trim duplicates 114 | newkeys = [] 115 | for k in keys: 116 | if not k in newkeys: 117 | newkeys.append(k) 118 | 119 | # Print all found keys 120 | for k in newkeys: 121 | if import_to_json: 122 | # TODO: Add keys to json 123 | pass 124 | 125 | if extract: 126 | print(k) 127 | 128 | 129 | return 0 130 | 131 | 132 | if __name__ == "__main__": 133 | print("This code is not intended to be executed directly!", file=sys.stderr) -------------------------------------------------------------------------------- /DeDRM_plugin/standalone/remove_drm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # CLI interface for the DeDRM plugin (useable without Calibre, too) 5 | # DRM removal 6 | 7 | from __future__ import absolute_import, print_function 8 | 9 | # Copyright © 2021 NoDRM 10 | 11 | """ 12 | 13 | NOTE: This code is not functional (yet). I started working on it a while ago 14 | to make a standalone version of the plugins that could work without Calibre, 15 | too, but for now there's only a rough code structure and no working code yet. 16 | 17 | Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll 18 | change in the future. 19 | 20 | """ 21 | 22 | #@@CALIBRE_COMPAT_CODE@@ 23 | 24 | import os, sys 25 | 26 | from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED 27 | from contextlib import closing 28 | 29 | from standalone.__init__ import print_opt, print_std_usage 30 | 31 | iswindows = sys.platform.startswith('win') 32 | isosx = sys.platform.startswith('darwin') 33 | 34 | def print_removedrm_help(): 35 | from __init__ import PLUGIN_NAME, PLUGIN_VERSION 36 | print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM") 37 | print() 38 | print("remove_drm: Remove DRM from one or multiple files") 39 | print() 40 | print_std_usage("remove_drm", " ... [ -o ] [ -f ]") 41 | 42 | print() 43 | print("Options: ") 44 | print_opt(None, "outputdir", "Folder to export the file(s) to") 45 | print_opt("o", "output", "File name to export the file to") 46 | print_opt("f", "force", "Overwrite output file if it already exists") 47 | print_opt(None, "overwrite", "Replace DRMed file with DRM-free file (implies --force)") 48 | 49 | 50 | def determine_file_type(file): 51 | # Returns a file type: 52 | # "PDF", "PDB", "MOBI", "TPZ", "LCP", "ADEPT", "ADEPT-PassHash", "KFX-ZIP", "ZIP" or None 53 | 54 | f = open(file, "rb") 55 | fdata = f.read(100) 56 | f.close() 57 | 58 | if fdata.startswith(b"PK\x03\x04"): 59 | pass 60 | # Either LCP, Adobe, or Amazon 61 | elif fdata.startswith(b"%PDF"): 62 | return "PDF" 63 | elif fdata[0x3c:0x3c+8] == b"PNRdPPrs" or fdata[0x3c:0x3c+8] == b"PDctPPrs": 64 | return "PDB" 65 | elif fdata[0x3c:0x3c+8] == b"BOOKMOBI" or fdata[0x3c:0x3c+8] == b"TEXtREAd": 66 | return "MOBI" 67 | elif fdata.startswith(b"TPZ"): 68 | return "TPZ" 69 | else: 70 | return None 71 | # Unknown file type 72 | 73 | 74 | # If it's a ZIP, determine the type. 75 | 76 | from lcpdedrm import isLCPbook 77 | if isLCPbook(file): 78 | return "LCP" 79 | 80 | from ineptepub import adeptBook, isPassHashBook 81 | if adeptBook(file): 82 | if isPassHashBook(file): 83 | return "ADEPT-PassHash" 84 | else: 85 | return "ADEPT" 86 | 87 | try: 88 | # Amazon / KFX-ZIP has a file that starts with b'\xeaDRMION\xee' in the ZIP. 89 | with closing(ZipFile(open(file, "rb"))) as book: 90 | for subfilename in book.namelist(): 91 | with book.open(subfilename) as subfile: 92 | data = subfile.read(8) 93 | if data == b'\xeaDRMION\xee': 94 | return "KFX-ZIP" 95 | except: 96 | pass 97 | 98 | return "ZIP" 99 | 100 | 101 | 102 | 103 | def dedrm_single_file(input_file, output_file): 104 | # When this runs, all the stupid file handling is done. 105 | # Just take the file at the absolute path "input_file" 106 | # and export it, DRM-free, to "output_file". 107 | 108 | # Use a temp file as input_file and output_file 109 | # might be identical. 110 | 111 | # The output directory might not exist yet. 112 | 113 | print("File " + input_file + " to " + output_file) 114 | 115 | # Okay, first check the file type and don't rely on the extension. 116 | try: 117 | ftype = determine_file_type(input_file) 118 | except: 119 | print("Can't determine file type for this file.") 120 | ftype = None 121 | 122 | if ftype is None: 123 | return 124 | 125 | 126 | 127 | 128 | 129 | def perform_action(params, files): 130 | output = None 131 | outputdir = None 132 | force = False 133 | overwrite_original = False 134 | 135 | 136 | if len(files) == 0: 137 | print_removedrm_help() 138 | return 0 139 | 140 | while len(params) > 0: 141 | p = params.pop(0) 142 | if p == "--output": 143 | output = params.pop(0) 144 | elif p == "--outputdir": 145 | outputdir = params.pop(0) 146 | elif p == "--force": 147 | force = True 148 | elif p == "--overwrite": 149 | overwrite_original = True 150 | force = True 151 | elif p == "--help": 152 | print_removedrm_help() 153 | return 0 154 | 155 | if overwrite_original and (output is not None or outputdir is not None): 156 | print("Can't use --overwrite together with --output or --outputdir.", file=sys.stderr) 157 | return 1 158 | 159 | if output is not None and os.path.isfile(output) and not force: 160 | print("Output file already exists. Use --force to overwrite.", file=sys.stderr) 161 | return 1 162 | 163 | 164 | if output is not None and len(files) > 1: 165 | print("Cannot set output file name if there's multiple input files.", file=sys.stderr) 166 | return 1 167 | 168 | if outputdir is not None and output is not None and os.path.isabs(output): 169 | print("--output parameter is absolute path despite --outputdir being set.", file=sys.stderr) 170 | print("Remove --outputdir, or give a relative path to --output.", file=sys.stderr) 171 | return 1 172 | 173 | 174 | 175 | for file in files: 176 | 177 | file = os.path.abspath(file) 178 | 179 | if not os.path.isfile(file): 180 | print("Skipping file " + file + " - not found.", file=sys.stderr) 181 | continue 182 | 183 | if overwrite_original: 184 | output_filename = file 185 | else: 186 | if output is not None: 187 | # Due to the check above, we DO only have one file here. 188 | if outputdir is not None and not os.path.isabs(output): 189 | output_filename = os.path.join(outputdir, output) 190 | else: 191 | output_filename = os.path.abspath(output) 192 | else: 193 | if outputdir is None: 194 | outputdir = os.getcwd() 195 | output_filename = os.path.join(outputdir, os.path.basename(file)) 196 | output_filename = os.path.abspath(output_filename) 197 | 198 | if output_filename == file: 199 | # If we export to the import folder, add a suffix to the file name. 200 | fn, f_ext = os.path.splitext(output_filename) 201 | output_filename = fn + "_nodrm" + f_ext 202 | 203 | 204 | 205 | if os.path.isfile(output_filename) and not force: 206 | print("Skipping file " + file + " because output file already exists (use --force).", file=sys.stderr) 207 | continue 208 | 209 | 210 | 211 | dedrm_single_file(file, output_filename) 212 | 213 | 214 | 215 | 216 | return 0 217 | 218 | 219 | if __name__ == "__main__": 220 | print("This code is not intended to be executed directly!", file=sys.stderr) -------------------------------------------------------------------------------- /DeDRM_plugin/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | #@@CALIBRE_COMPAT_CODE@@ 5 | 6 | import sys 7 | 8 | __license__ = 'GPL v3' 9 | 10 | def uStrCmp (s1, s2, caseless=False): 11 | import unicodedata as ud 12 | if sys.version_info[0] == 2: 13 | str1 = s1 if isinstance(s1, unicode) else unicode(s1) 14 | str2 = s2 if isinstance(s2, unicode) else unicode(s2) 15 | else: 16 | str1 = s1 if isinstance(s1, str) else str(s1) 17 | str2 = s2 if isinstance(s2, str) else str(s2) 18 | 19 | if caseless: 20 | return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower()) 21 | else: 22 | return ud.normalize('NFC', str1) == ud.normalize('NFC', str2) 23 | 24 | 25 | 26 | # Wrap a stream so that output gets flushed immediately 27 | # and also make sure that any unicode strings get safely 28 | # encoded using "replace" before writing them. 29 | class SafeUnbuffered: 30 | def __init__(self, stream): 31 | self.stream = stream 32 | self.encoding = stream.encoding 33 | if self.encoding == None: 34 | self.encoding = "utf-8" 35 | def write(self, data): 36 | if isinstance(data,str) or isinstance(data,unicode): 37 | # str for Python3, unicode for Python2 38 | data = data.encode(self.encoding,"replace") 39 | try: 40 | buffer = getattr(self.stream, 'buffer', self.stream) 41 | # self.stream.buffer for Python3, self.stream for Python2 42 | buffer.write(data) 43 | buffer.flush() 44 | except: 45 | # We can do nothing if a write fails 46 | raise 47 | def __getattr__(self, attr): 48 | return getattr(self.stream, attr) 49 | -------------------------------------------------------------------------------- /DeDRM_plugin/wineutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __license__ = 'GPL v3' 5 | 6 | # Standard Python modules. 7 | import os, sys, re, hashlib, traceback 8 | 9 | #@@CALIBRE_COMPAT_CODE@@ 10 | 11 | from __init__ import PLUGIN_NAME, PLUGIN_VERSION 12 | 13 | 14 | class NoWinePython3Exception(Exception): 15 | pass 16 | 17 | 18 | class WinePythonCLI: 19 | py3_test = "import sys; sys.exit(0 if (sys.version_info.major==3) else 1)" 20 | def __init__(self, wineprefix=""): 21 | import subprocess 22 | 23 | if wineprefix != "": 24 | wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix))) 25 | 26 | if wineprefix != "" and os.path.exists(wineprefix): 27 | self.wineprefix = wineprefix 28 | else: 29 | self.wineprefix = None 30 | 31 | candidate_execs = [ 32 | ["wine", "py.exe", "-3"], 33 | ["wine", "python3.exe"], 34 | ["wine", "python.exe"], 35 | ["wine", "C:\\Python27\\python.exe"], # Should likely be removed 36 | ] 37 | for e in candidate_execs: 38 | self.python_exec = e 39 | try: 40 | self.check_call(["-c", self.py3_test]) 41 | print("{0} v{1}: Python3 exec found as {2}".format( 42 | PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec) 43 | )) 44 | return None 45 | except subprocess.CalledProcessError as e: 46 | if e.returncode == 1: 47 | print("{0} v{1}: {2} is not python3".format( 48 | PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec) 49 | )) 50 | elif e.returncode == 53: 51 | print("{0} v{1}: {2} does not exist".format( 52 | PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec) 53 | )) 54 | raise NoWinePython3Exception("Could not find python3 executable on specified wine prefix") 55 | 56 | 57 | def check_call(self, cli_args): 58 | import subprocess 59 | 60 | env_dict = os.environ 61 | env_dict["PYTHONPATH"] = "" 62 | if self.wineprefix is not None: 63 | env_dict["WINEPREFIX"] = self.wineprefix 64 | 65 | subprocess.check_call(self.python_exec + cli_args, env=env_dict, 66 | stdin=None, stdout=sys.stdout, 67 | stderr=subprocess.STDOUT, close_fds=False, 68 | bufsize=1) 69 | 70 | 71 | def WineGetKeys(scriptpath, extension, wineprefix=""): 72 | 73 | if extension == ".k4i": 74 | import json 75 | 76 | try: 77 | pyexec = WinePythonCLI(wineprefix) 78 | except NoWinePython3Exception: 79 | print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix)) 80 | return [], [] 81 | 82 | basepath, script = os.path.split(scriptpath) 83 | print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script)) 84 | 85 | outdirpath = os.path.join(basepath, "winekeysdir") 86 | if not os.path.exists(outdirpath): 87 | os.makedirs(outdirpath) 88 | 89 | if wineprefix != "": 90 | wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix))) 91 | 92 | try: 93 | result = pyexec.check_call([scriptpath, outdirpath]) 94 | except Exception as e: 95 | print("{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])) 96 | 97 | # try finding winekeys anyway, even if above code errored 98 | winekeys = [] 99 | winekey_names = [] 100 | # get any files with extension in the output dir 101 | files = [f for f in os.listdir(outdirpath) if f.endswith(extension)] 102 | for filename in files: 103 | try: 104 | fpath = os.path.join(outdirpath, filename) 105 | with open(fpath, 'rb') as keyfile: 106 | if extension == ".k4i": 107 | new_key_value = json.loads(keyfile.read()) 108 | else: 109 | new_key_value = keyfile.read() 110 | winekeys.append(new_key_value) 111 | winekey_names.append(filename) 112 | except: 113 | print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename)) 114 | traceback.print_exc() 115 | os.remove(fpath) 116 | print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files")) 117 | return winekeys, winekey_names 118 | -------------------------------------------------------------------------------- /DeDRM_plugin/zeroedzipinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | """ 6 | Python 3's "zipfile" has an annoying bug where the `external_attr` field 7 | of a ZIP file cannot be set to 0. However, if the original DRMed ZIP has 8 | that set to 0 then we want the DRM-free ZIP to have that as 0, too. 9 | See https://github.com/python/cpython/issues/87713 10 | 11 | We cannot just set the "external_attr" to 0 as the code to save the ZIP 12 | resets that variable. 13 | 14 | So, here's a class that inherits from ZipInfo and ensures that EVERY 15 | read access to that variable will return a 0 ... 16 | 17 | """ 18 | 19 | import zipfile 20 | 21 | class ZeroedZipInfo(zipfile.ZipInfo): 22 | def __init__(self, zinfo): 23 | for k in self.__slots__: 24 | if hasattr(zinfo, k): 25 | setattr(self, k, getattr(zinfo, k)) 26 | 27 | def __getattribute__(self, name): 28 | if name == "external_attr": 29 | return 0 30 | return object.__getattribute__(self, name) 31 | -------------------------------------------------------------------------------- /DeDRM_plugin/zipfix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # zipfix.py 5 | # Copyright © 2010-2020 by Apprentice Harper et al. 6 | 7 | # Released under the terms of the GNU General Public Licence, version 3 8 | # 9 | 10 | # Revision history: 11 | # 1.0 - Initial release 12 | # 1.1 - Updated to handle zip file metadata correctly 13 | # 2.0 - Python 3 for calibre 5.0 14 | 15 | """ 16 | Re-write zip (or ePub) fixing problems with file names (and mimetype entry). 17 | """ 18 | 19 | 20 | __license__ = 'GPL v3' 21 | __version__ = "1.1" 22 | 23 | import sys, os 24 | 25 | #@@CALIBRE_COMPAT_CODE@@ 26 | 27 | import zlib 28 | import zipfilerugged 29 | from zipfilerugged import ZipInfo, ZeroedZipInfo 30 | import getopt 31 | from struct import unpack 32 | 33 | 34 | _FILENAME_LEN_OFFSET = 26 35 | _EXTRA_LEN_OFFSET = 28 36 | _FILENAME_OFFSET = 30 37 | _MAX_SIZE = 64 * 1024 38 | _MIMETYPE = 'application/epub+zip' 39 | 40 | 41 | class fixZip: 42 | def __init__(self, zinput, zoutput): 43 | self.ztype = 'zip' 44 | if zinput.lower().find('.epub') >= 0 : 45 | self.ztype = 'epub' 46 | self.inzip = zipfilerugged.ZipFile(zinput,'r') 47 | self.outzip = zipfilerugged.ZipFile(zoutput,'w') 48 | # open the input zip for reading only as a raw file 49 | self.bzf = open(zinput,'rb') 50 | 51 | def getlocalname(self, zi): 52 | local_header_offset = zi.header_offset 53 | self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) 54 | leninfo = self.bzf.read(2) 55 | local_name_length, = unpack(' 0: 64 | if len(cmpdata) > _MAX_SIZE : 65 | newdata = cmpdata[0:_MAX_SIZE] 66 | cmpdata = cmpdata[_MAX_SIZE:] 67 | else: 68 | newdata = cmpdata 69 | cmpdata = b'' 70 | newdata = dc.decompress(newdata) 71 | unprocessed = dc.unconsumed_tail 72 | if len(unprocessed) == 0: 73 | newdata += dc.flush() 74 | data += newdata 75 | cmpdata += unprocessed 76 | unprocessed = b'' 77 | return data 78 | 79 | def getfiledata(self, zi): 80 | # get file name length and exta data length to find start of file data 81 | local_header_offset = zi.header_offset 82 | 83 | self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) 84 | leninfo = self.bzf.read(2) 85 | local_name_length, = unpack('Plugins 47 | ''' 48 | return True 49 | 50 | def config_widget(self): 51 | ''' 52 | Implement this method and :meth:`save_settings` in your plugin to 53 | use a custom configuration dialog. 54 | 55 | This method, if implemented, must return a QWidget. The widget can have 56 | an optional method validate() that takes no arguments and is called 57 | immediately after the user clicks OK. Changes are applied if and only 58 | if the method returns True. 59 | 60 | If for some reason you cannot perform the configuration at this time, 61 | return a tuple of two strings (message, details), these will be 62 | displayed as a warning dialog to the user and the process will be 63 | aborted. 64 | 65 | The base class implementation of this method raises NotImplementedError 66 | so by default no user configuration is possible. 67 | ''' 68 | if self.actual_plugin_: 69 | from calibre_plugins.obok_dedrm.config import ConfigWidget 70 | return ConfigWidget(self.actual_plugin_) 71 | 72 | def save_settings(self, config_widget): 73 | ''' 74 | Save the settings specified by the user with config_widget. 75 | ''' 76 | config_widget.save_settings() 77 | -------------------------------------------------------------------------------- /Obok_plugin/images/obok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/images/obok.png -------------------------------------------------------------------------------- /Obok_plugin/obok/__init__.py: -------------------------------------------------------------------------------- 1 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 2 | 3 | __license__ = 'GPL v3' 4 | __docformat__ = 'restructuredtext en' 5 | -------------------------------------------------------------------------------- /Obok_plugin/obok/legacy_obok.py: -------------------------------------------------------------------------------- 1 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 2 | 3 | __license__ = 'GPL v3' 4 | __docformat__ = 'restructuredtext en' 5 | 6 | import os, sys 7 | import binascii, hashlib, re, string 8 | 9 | class legacy_obok(object): 10 | def __init__(self): 11 | self._userkey = '' 12 | 13 | @property 14 | def get_legacy_cookie_id(self): 15 | if self._userkey != '': 16 | return self._userkey 17 | self._userkey = self.__oldcookiedeviceid() 18 | return self._userkey 19 | 20 | def __bytearraytostring(self, bytearr): 21 | wincheck = re.match('@ByteArray\\((.+)\\)', bytearr) 22 | if wincheck: 23 | return wincheck.group(1) 24 | return bytearr 25 | 26 | def plist_to_dictionary(self, filename): 27 | from subprocess import Popen, PIPE 28 | from plistlib import readPlistFromString 29 | 'Pipe the binary plist through plutil and parse the xml output' 30 | with open(filename, 'rb') as f: 31 | content = f.read() 32 | args = ['plutil', '-convert', 'xml1', '-o', '-', '--', '-'] 33 | p = Popen(args, stdin=PIPE, stdout=PIPE) 34 | p.stdin.write(content) 35 | out, err = p.communicate() 36 | return readPlistFromString(out) 37 | 38 | def __oldcookiedeviceid(self): 39 | '''Optionally attempt to get a device id using the old cookie method. 40 | Must have winreg installed on Windows machines for successful key retrieval.''' 41 | wsuid = '' 42 | pwsdid = '' 43 | try: 44 | if sys.platform.startswith('win'): 45 | try: 46 | import winreg 47 | except ImportError: 48 | import _winreg as winreg 49 | regkey_browser = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser') 50 | cookies = winreg.QueryValueEx(regkey_browser, 'cookies') 51 | bytearrays = cookies[0] 52 | elif sys.platform.startswith('darwin'): 53 | prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist') 54 | cookies = self.plist_to_dictionary(prefs) 55 | bytearrays = cookies['Browser.cookies'] 56 | for bytearr in bytearrays: 57 | cookie = self.__bytearraytostring(bytearr) 58 | wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie) 59 | if(wsuidcheck): 60 | wsuid = wsuidcheck.group(1) 61 | pwsdidcheck = re.match('^pwsdid=([0-9a-f-]+)', cookie) 62 | if (pwsdidcheck): 63 | pwsdid = pwsdidcheck.group(1) 64 | if (wsuid == '' or pwsdid == ''): 65 | return None 66 | preuserkey = string.join((pwsdid, wsuid), '') 67 | userkey = hashlib.sha256(preuserkey).hexdigest() 68 | return binascii.a2b_hex(userkey[32:]) 69 | except KeyError: 70 | print ('No "cookies" key found in Kobo plist: no legacy user key found.') 71 | return None 72 | except: 73 | print ('Error parsing Kobo plist: no legacy user key found.') 74 | return None 75 | -------------------------------------------------------------------------------- /Obok_plugin/obok_dedrm_Help.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Obok DeDRM Plugin Configuration 6 | 7 | 8 | 9 | 10 |

Obok DeDRM Plugin

11 |

(version 10.0.9 / 10.1.0 RC1)

12 | 13 |

Installation:

14 | 15 |

The ususal method of Preferences -> Plugins -> Load plugin from file.

16 | 17 | 18 |

Configuration:

19 | 20 |

There is no configuration (other than to choose what menus to add obok to)

21 | 22 | 23 |

Troubleshooting:

24 | 25 |

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

26 | 27 |

Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin 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 Apprentice Alf's blog.

28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Obok_plugin/plugin-import-name-obok_dedrm.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/plugin-import-name-obok_dedrm.txt -------------------------------------------------------------------------------- /Obok_plugin/translations/ar.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/ar.mo -------------------------------------------------------------------------------- /Obok_plugin/translations/de.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/de.mo -------------------------------------------------------------------------------- /Obok_plugin/translations/de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: obok\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-10-19 10:28+0200\n" 11 | "PO-Revision-Date: 2014-10-23 14:43+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: friends of obok\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 1.6.10\n" 20 | 21 | #: common_utils.py:220 22 | msgid "Help" 23 | msgstr "Hilfe" 24 | 25 | #: common_utils.py:229 utilities.py:207 26 | msgid "Restart required" 27 | msgstr "Neustart erforderlich" 28 | 29 | #: common_utils.py:230 utilities.py:208 30 | msgid "" 31 | "Title image not found - you must restart Calibre before using this plugin!" 32 | msgstr "" 33 | "Das Abbild wurde nicht gefunden. - vor der Verwendung dieses Calibre Plugin " 34 | "is ein Neustart erforderlich!" 35 | 36 | #: common_utils.py:316 37 | msgid "Undefined" 38 | msgstr "Undefiniert" 39 | 40 | #: config.py:25 41 | msgid "" 42 | "

Default behavior when duplicates are detected. None of the choices will " 43 | "cause calibre ebooks to be overwritten" 44 | msgstr "" 45 | "

Standardverhalten, wenn Duplikate erkannt werden. Keine der " 46 | "Entscheidungen werden ebooks verursachen das sie überschrieben werden." 47 | 48 | #: dialogs.py:58 49 | msgid "Obok DeDRM" 50 | msgstr "Obok DeDRM" 51 | 52 | #: dialogs.py:68 53 | msgid "Help" 54 | msgstr "Hilfe" 55 | 56 | #: dialogs.py:82 57 | msgid "Select All" 58 | msgstr "Alles markieren" 59 | 60 | #: dialogs.py:83 61 | msgid "Select all books to add them to the calibre library." 62 | msgstr "Wählen Sie alle Bücher, um sie zu Calibre Bibliothek hinzuzufügen." 63 | 64 | #: dialogs.py:85 65 | msgid "All with DRM" 66 | msgstr "Alle mit DRM" 67 | 68 | #: dialogs.py:86 69 | msgid "Select all books with DRM." 70 | msgstr "Wählen Sie alle Bücher mit DRM." 71 | 72 | #: dialogs.py:88 73 | msgid "All DRM free" 74 | msgstr "Alle ohne DRM" 75 | 76 | #: dialogs.py:89 77 | msgid "Select all books without DRM." 78 | msgstr "Wählen Sie alle Bücher ohne DRM." 79 | 80 | #: dialogs.py:139 81 | msgid "Title" 82 | msgstr "Titel" 83 | 84 | #: dialogs.py:139 85 | msgid "Author" 86 | msgstr "Autor" 87 | 88 | #: dialogs.py:139 89 | msgid "Series" 90 | msgstr "Reihe" 91 | 92 | #: dialogs.py:362 93 | msgid "Copy to clipboard" 94 | msgstr "In Zwischenablage kopieren" 95 | 96 | #: dialogs.py:390 97 | msgid "View Report" 98 | msgstr "Bericht anzeigen" 99 | 100 | #: __init__.py:24 101 | msgid "Removes DRM from Kobo kepubs and adds them to the library." 102 | msgstr "Entfernt DRM von Kobo kepubs und fügt sie zu Bibliothek hinzu." 103 | -------------------------------------------------------------------------------- /Obok_plugin/translations/es.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/es.mo -------------------------------------------------------------------------------- /Obok_plugin/translations/nl.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/nl.mo -------------------------------------------------------------------------------- /Obok_plugin/translations/nl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: obok\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-10-19 10:28+0200\n" 11 | "PO-Revision-Date: 2014-10-23 14:08+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: friends of obok\n" 14 | "Language: nl\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 1.6.10\n" 20 | 21 | #: common_utils.py:220 22 | msgid "Help" 23 | msgstr "Help" 24 | 25 | #: common_utils.py:229 utilities.py:207 26 | msgid "Restart required" 27 | msgstr "Opnieuw opstarten vereist" 28 | 29 | #: common_utils.py:230 utilities.py:208 30 | msgid "" 31 | "Title image not found - you must restart Calibre before using this plugin!" 32 | msgstr "" 33 | "Afbeelding niet gevonden. - Calibre moet opnieuw opgestart worden voordat " 34 | "deze plugin kan worden gebruikt!" 35 | 36 | #: common_utils.py:316 37 | msgid "Undefined" 38 | msgstr "Niet gedefinieerd" 39 | 40 | #: config.py:25 41 | msgid "" 42 | "

Default behavior when duplicates are detected. None of the choices will " 43 | "cause calibre ebooks to be overwritten" 44 | msgstr "" 45 | "

Standaard gedrag wanneer er duplicaten worden geconstateerd. Geen van de " 46 | "opties zal reeds bestaande ebooks in de Calibre bibliotheek overschrijven." 47 | 48 | #: dialogs.py:58 49 | msgid "Obok DeDRM" 50 | msgstr "Obok DeDRM" 51 | 52 | #: dialogs.py:68 53 | msgid "Help" 54 | msgstr "Help" 55 | 56 | #: dialogs.py:82 57 | msgid "Select All" 58 | msgstr "Alles selecteren" 59 | 60 | #: dialogs.py:83 61 | msgid "Select all books to add them to the calibre library." 62 | msgstr "Alle boeken selecteren om ze aan de Calibre bibliotheek toe te voegen." 63 | 64 | #: dialogs.py:85 65 | msgid "All with DRM" 66 | msgstr "Alle met DRM" 67 | 68 | #: dialogs.py:86 69 | msgid "Select all books with DRM." 70 | msgstr "Alle boeken met DRM selecteren." 71 | 72 | #: dialogs.py:88 73 | msgid "All DRM free" 74 | msgstr "Alle zonder DRM" 75 | 76 | #: dialogs.py:89 77 | msgid "Select all books without DRM." 78 | msgstr "Alle boeken zonder DRM selecteren." 79 | 80 | #: dialogs.py:139 81 | msgid "Title" 82 | msgstr "Titel" 83 | 84 | #: dialogs.py:139 85 | msgid "Author" 86 | msgstr "Auteur" 87 | 88 | #: dialogs.py:139 89 | msgid "Series" 90 | msgstr "Reeks/serie" 91 | 92 | #: dialogs.py:362 93 | msgid "Copy to clipboard" 94 | msgstr "Naar het Klembord kopiëren" 95 | 96 | #: dialogs.py:390 97 | msgid "View Report" 98 | msgstr "Rapport weergeven" 99 | 100 | #: __init__.py:24 101 | msgid "Removes DRM from Kobo kepubs and adds them to the library." 102 | msgstr "Verwijdert de DRM van Kobo kepubs en voegt ze toe aan de bibliotheek." 103 | -------------------------------------------------------------------------------- /Obok_plugin/translations/pt.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/pt.mo -------------------------------------------------------------------------------- /Obok_plugin/translations/sv.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Obok_plugin/translations/sv.mo -------------------------------------------------------------------------------- /Obok_plugin/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 4 | 5 | __license__ = 'GPL v3' 6 | __docformat__ = 'restructuredtext en' 7 | 8 | 9 | import os, struct, time 10 | try: 11 | from StringIO import StringIO 12 | except ImportError: 13 | from io import StringIO 14 | from traceback import print_exc 15 | 16 | from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem) 17 | 18 | from calibre.utils.config import config_dir 19 | from calibre.constants import iswindows, DEBUG 20 | from calibre import prints 21 | from calibre.gui2 import (error_dialog, gprefs) 22 | from calibre.gui2.actions import menu_action_unique_name 23 | 24 | from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, 25 | PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION) 26 | 27 | plugin_ID = None 28 | plugin_icon_resources = {} 29 | 30 | try: 31 | from calibre.gui2 import QVariant 32 | del QVariant 33 | except ImportError: 34 | is_qt4 = False 35 | convert_qvariant = lambda x: x 36 | else: 37 | is_qt4 = True 38 | 39 | def convert_qvariant(x): 40 | vt = x.type() 41 | if vt == x.String: 42 | return x.toString() 43 | if vt == x.List: 44 | return [convert_qvariant(i) for i in x.toList()] 45 | return x.toPyObject() 46 | 47 | BASE_TIME = None 48 | def debug_print(*args): 49 | global BASE_TIME 50 | if BASE_TIME is None: 51 | BASE_TIME = time.time() 52 | if DEBUG: 53 | prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args) 54 | 55 | try: 56 | debug_print("obok::utilities.py - loading translations") 57 | load_translations() 58 | except NameError: 59 | debug_print("obok::utilities.py - exception when loading translations") 60 | pass # load_translations() added in calibre 1.9 61 | 62 | def format_plural(number, possessive=False): 63 | ''' 64 | Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations 65 | 66 | :param: number: variable that represents the count/len of something 67 | ''' 68 | if not possessive: 69 | return '' if number == 1 else 's' 70 | return '\'s' if number == 1 else 's\'' 71 | 72 | 73 | def set_plugin_icon_resources(name, resources): 74 | ''' 75 | Set our global store of plugin name and icon resources for sharing between 76 | the InterfaceAction class which reads them and the ConfigWidget 77 | if needed for use on the customization dialog for this plugin. 78 | ''' 79 | global plugin_icon_resources, plugin_ID 80 | plugin_ID = name 81 | plugin_icon_resources = resources 82 | 83 | def get_icon(icon_name): 84 | ''' 85 | Retrieve a QIcon for the named image from the zip file if it exists, 86 | or if not then from Calibre's image cache. 87 | ''' 88 | if icon_name: 89 | pixmap = get_pixmap(icon_name) 90 | if pixmap is None: 91 | # Look in Calibre's cache for the icon 92 | return QIcon(I(icon_name)) 93 | else: 94 | return QIcon(pixmap) 95 | return QIcon() 96 | 97 | def get_pixmap(icon_name): 98 | ''' 99 | Retrieve a QPixmap for the named image 100 | Any icons belonging to the plugin must be prefixed with 'images/' 101 | ''' 102 | if not icon_name.startswith('images/'): 103 | # We know this is definitely not an icon belonging to this plugin 104 | pixmap = QPixmap() 105 | pixmap.load(I(icon_name)) 106 | return pixmap 107 | 108 | # Check to see whether the icon exists as a Calibre resource 109 | # This will enable skinning if the user stores icons within a folder like: 110 | # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ 111 | if plugin_ID: 112 | local_images_dir = get_local_images_dir(plugin_ID) 113 | local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', '')) 114 | if os.path.exists(local_image_path): 115 | pixmap = QPixmap() 116 | pixmap.load(local_image_path) 117 | return pixmap 118 | 119 | # As we did not find an icon elsewhere, look within our zip resources 120 | if icon_name in plugin_icon_resources: 121 | pixmap = QPixmap() 122 | pixmap.loadFromData(plugin_icon_resources[icon_name]) 123 | return pixmap 124 | return None 125 | 126 | def get_local_images_dir(subfolder=None): 127 | ''' 128 | Returns a path to the user's local resources/images folder 129 | If a subfolder name parameter is specified, appends this to the path 130 | ''' 131 | images_dir = os.path.join(config_dir, 'resources/images') 132 | if subfolder: 133 | images_dir = os.path.join(images_dir, subfolder) 134 | if iswindows: 135 | images_dir = os.path.normpath(images_dir) 136 | return images_dir 137 | 138 | def showErrorDlg(errmsg, parent, trcbk=False): 139 | ''' 140 | Wrapper method for calibre's error_dialog 141 | ''' 142 | if trcbk: 143 | error= '' 144 | f=StringIO() 145 | print_exc(file=f) 146 | error_mess = f.getvalue().splitlines() 147 | for line in error_mess: 148 | error = error + str(line) + '\n' 149 | errmsg = errmsg + '\n\n' + error 150 | return error_dialog(parent, _(PLUGIN_NAME + ' v' + PLUGIN_VERSION), 151 | _(errmsg), show=True) 152 | 153 | class SizePersistedDialog(QDialog): 154 | ''' 155 | This dialog is a base class for any dialogs that want their size/position 156 | restored when they are next opened. 157 | ''' 158 | def __init__(self, parent, unique_pref_name): 159 | QDialog.__init__(self, parent) 160 | self.unique_pref_name = unique_pref_name 161 | self.geom = gprefs.get(unique_pref_name, None) 162 | self.finished.connect(self.dialog_closing) 163 | 164 | def resize_dialog(self): 165 | if self.geom is None: 166 | self.resize(self.sizeHint()) 167 | else: 168 | self.restoreGeometry(self.geom) 169 | 170 | def dialog_closing(self, result): 171 | geom = bytearray(self.saveGeometry()) 172 | gprefs[self.unique_pref_name] = geom 173 | self.persist_custom_prefs() 174 | 175 | def persist_custom_prefs(self): 176 | ''' 177 | Invoked when the dialog is closing. Override this function to call 178 | save_custom_pref() if you have a setting you want persisted that you can 179 | retrieve in your __init__() using load_custom_pref() when next opened 180 | ''' 181 | pass 182 | 183 | def load_custom_pref(self, name, default=None): 184 | return gprefs.get(self.unique_pref_name+':'+name, default) 185 | 186 | def save_custom_pref(self, name, value): 187 | gprefs[self.unique_pref_name+':'+name] = value 188 | 189 | class ImageTitleLayout(QHBoxLayout): 190 | ''' 191 | A reusable layout widget displaying an image followed by a title 192 | ''' 193 | def __init__(self, parent, icon_name, title): 194 | ''' 195 | :param parent: Parent gui 196 | :param icon_name: Path to plugin image resource 197 | :param title: String to be displayed beside the image 198 | ''' 199 | QHBoxLayout.__init__(self) 200 | self.title_image_label = QLabel(parent) 201 | self.update_title_icon(icon_name) 202 | self.addWidget(self.title_image_label) 203 | 204 | title_font = QFont() 205 | title_font.setPointSize(16) 206 | shelf_label = QLabel(title, parent) 207 | shelf_label.setFont(title_font) 208 | self.addWidget(shelf_label) 209 | self.insertStretch(-1) 210 | 211 | def update_title_icon(self, icon_name): 212 | pixmap = get_pixmap(icon_name) 213 | if pixmap is None: 214 | error_dialog(self.parent(), _('Restart required'), 215 | _('Title image not found - you must restart Calibre before using this plugin!'), show=True) 216 | else: 217 | self.title_image_label.setPixmap(pixmap) 218 | self.title_image_label.setMaximumSize(32, 32) 219 | self.title_image_label.setScaledContents(True) 220 | 221 | 222 | class ReadOnlyTableWidgetItem(QTableWidgetItem): 223 | 224 | def __init__(self, text): 225 | if text is None: 226 | text = '' 227 | QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType) 228 | self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) 229 | -------------------------------------------------------------------------------- /Other_Tools/B_and_N_Download_Helper/BN-Dload.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name BN-Dload 3 | // @namespace http://www.mailinator.com/J-man 4 | // @include https://mynook.barnesandnoble.com/library.html* 5 | // @grant none 6 | // @version 20121119 7 | // ==/UserScript== 8 | 9 | function doIt() { 10 | if ($('#adl1').length == 0) { 11 | $('[action$="deleteItem"]').each(function(index) { 12 | if ($(this).parent().find('[action$="EDSDeliverItem.aspx"]').length == 0) { 13 | var delid = $(this).find('input').attr('value'); 14 | $(this).after('

'); 15 | } 16 | }); 17 | 18 | } 19 | 20 | setTimeout (function() { 21 | doIt(); 22 | }, 3000 ); 23 | } 24 | 25 | doIt(); 26 | 27 | -------------------------------------------------------------------------------- /Other_Tools/B_and_N_Download_Helper/BN-Dload.user_ReadMe.txt: -------------------------------------------------------------------------------- 1 | INTRODUCTION 2 | ============ 3 | 4 | To obtain unencrypted content from the B&N, you have to download it directly from the website. Unrooted Nook devices will not let you save your content to your PC. 5 | 6 | If the downloaded file is encrypted, install and configure the ignoble plugin in Calibre to decrypt that. 7 | 8 | 9 | DOWNLOAD HIDDEN FILES FROM B&N 10 | ------------------------------ 11 | 12 | Some content is not downloadable from the B&N website, notably magazines. A Greasemonkey script (details below) modifies the myNook page of the Barnes and Noble website to show a download button for normally non-downloadable content. This will work until Barnes & Noble changes their website. 13 | 14 | Prerequisites 15 | ------------- 16 | 1) Firefox: http://www.getfirefox.com 17 | 2) Greasemokey extension: https://addons.mozilla.org/nl/firefox/addon/greasemonkey/ 18 | 19 | One time installation 20 | --------------------- 21 | 1) Install Firefox if not already done so 22 | 2) Follow the above link to GreaseMonkey and click Add to Firefox 23 | 3) Restart Firefox 24 | 4) Go to http://userscripts.org/scripts/source/152985.user.js 25 | 5) A popup should appear, stating you are about to install a GreaseMonkey user script. 26 | 6) Click on install 27 | 28 | Use 29 | --- 30 | 1) Log in into your B&N account 31 | 2) Go to MyNook 32 | 3) An “Alternative download” should appear next to normally non-downloadable content. Note that this will not work for content such as Nook applications, and some children books. 33 | -------------------------------------------------------------------------------- /Other_Tools/DRM_Key_Scripts/Kindle_for_iOS/kindleiospidgen.pyw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import with_statement 5 | 6 | # kindleforios4key.py 7 | # Copyright © 2013 by Apprentice Alf 8 | # Portions Copyright © 2007, 2009 Igor Skochinsky 9 | 10 | # Revision history: 11 | # 1.0 - Generates fixed PID for Kindle for iOS 3.1.1 running on iOS 4.x 12 | 13 | 14 | """ 15 | Generate fixed PID for Kindle for iOS 3.1.1 16 | """ 17 | 18 | __license__ = 'GPL v3' 19 | __version__ = '1.0' 20 | 21 | import sys, os 22 | import getopt 23 | import binascii 24 | 25 | # Wrap a stream so that output gets flushed immediately 26 | # and also make sure that any unicode strings get 27 | # encoded using "replace" before writing them. 28 | class SafeUnbuffered: 29 | def __init__(self, stream): 30 | self.stream = stream 31 | self.encoding = stream.encoding 32 | if self.encoding == None: 33 | self.encoding = "utf-8" 34 | def write(self, data): 35 | if isinstance(data,str) or isinstance(data,unicode): 36 | # str for Python3, unicode for Python2 37 | data = data.encode(self.encoding,"replace") 38 | try: 39 | buffer = getattr(self.stream, 'buffer', self.stream) 40 | # self.stream.buffer for Python3, self.stream for Python2 41 | buffer.write(data) 42 | buffer.flush() 43 | except: 44 | # We can do nothing if a write fails 45 | raise 46 | def __getattr__(self, attr): 47 | return getattr(self.stream, attr) 48 | 49 | 50 | iswindows = sys.platform.startswith('win') 51 | isosx = sys.platform.startswith('darwin') 52 | 53 | def unicode_argv(): 54 | if iswindows: 55 | # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode 56 | # strings. 57 | 58 | # Versions 2.x of Python don't support Unicode in sys.argv on 59 | # Windows, with the underlying Windows API instead replacing multi-byte 60 | # characters with '?'. 61 | 62 | 63 | from ctypes import POINTER, byref, cdll, c_int, windll 64 | from ctypes.wintypes import LPCWSTR, LPWSTR 65 | 66 | GetCommandLineW = cdll.kernel32.GetCommandLineW 67 | GetCommandLineW.argtypes = [] 68 | GetCommandLineW.restype = LPCWSTR 69 | 70 | CommandLineToArgvW = windll.shell32.CommandLineToArgvW 71 | CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] 72 | CommandLineToArgvW.restype = POINTER(LPWSTR) 73 | 74 | cmd = GetCommandLineW() 75 | argc = c_int(0) 76 | argv = CommandLineToArgvW(cmd, byref(argc)) 77 | if argc.value > 0: 78 | # Remove Python executable and commands if present 79 | start = argc.value - len(sys.argv) 80 | return [argv[i] for i in 81 | xrange(start, argc.value)] 82 | # if we don't have any arguments at all, just pass back script name 83 | # this should never happen 84 | return [u"mobidedrm.py"] 85 | else: 86 | argvencoding = sys.stdin.encoding 87 | if argvencoding == None: 88 | argvencoding = "utf-8" 89 | return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] 90 | 91 | import hashlib 92 | 93 | def SHA256(message): 94 | ctx = hashlib.sha256() 95 | ctx.update(message) 96 | return ctx.digest() 97 | 98 | def crc32(s): 99 | return (~binascii.crc32(s,-1))&0xFFFFFFFF 100 | 101 | letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' 102 | 103 | def checksumPid(s): 104 | crc = crc32(s) 105 | crc = crc ^ (crc >> 16) 106 | res = s 107 | l = len(letters) 108 | for i in (0,1): 109 | b = crc & 0xff 110 | pos = (b // l) ^ (b % l) 111 | res += letters[pos%l] 112 | crc >>= 8 113 | 114 | return res 115 | 116 | def pidFromSerial(s, l): 117 | crc = crc32(s) 118 | 119 | arr1 = [0]*l 120 | for i in xrange(len(s)): 121 | arr1[i%l] ^= ord(s[i]) 122 | 123 | crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] 124 | for i in xrange(l): 125 | arr1[i] ^= crc_bytes[i&3] 126 | 127 | pid = '' 128 | for i in xrange(l): 129 | b = arr1[i] & 0xff 130 | pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] 131 | 132 | return pid 133 | 134 | def generatekeys(email, mac): 135 | keys = [] 136 | email = email.encode('utf-8').lower() 137 | mac = mac.encode('utf-8').lower() 138 | cleanmac = "".join(c if (c in "0123456789abcdef") else "" for c in mac) 139 | lowermac = cleanmac.lower() 140 | #print lowermac 141 | keyseed = lowermac + email.encode('utf-8') 142 | #print keyseed 143 | keysha256 = SHA256(keyseed) 144 | keybase64 = keysha256.encode('base64') 145 | #print keybase64 146 | cleankeybase64 = "".join(c if (c in "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") else "0" for c in keybase64) 147 | #print cleankeybase64 148 | pseudoudid = cleankeybase64[:40] 149 | #print pseudoudid 150 | keys.append(pidFromSerial(pseudoudid.encode("utf-8"),8)) 151 | return keys 152 | 153 | # interface for Python DeDRM 154 | # returns single key or multiple keys, depending on path or file passed in 155 | def getkey(email, mac, outpath): 156 | keys = generatekeys(email,mac) 157 | if len(keys) > 0: 158 | if not os.path.isdir(outpath): 159 | outfile = outpath 160 | with file(outfile, 'w') as keyfileout: 161 | keyfileout.write(keys[0]) 162 | print u"Saved a key to {0}".format(outfile) 163 | else: 164 | keycount = 0 165 | for key in keys: 166 | while True: 167 | keycount += 1 168 | outfile = os.path.join(outpath,u"kindleios{0:d}.pid".format(keycount)) 169 | if not os.path.exists(outfile): 170 | break 171 | with file(outfile, 'w') as keyfileout: 172 | keyfileout.write(key) 173 | print u"Saved a key to {0}".format(outfile) 174 | return True 175 | return False 176 | 177 | def usage(progname): 178 | print u"Generates the key for Kindle for iOS 3.1.1" 179 | print u"Requires email address of Amazon acccount" 180 | print u"And MAC address for iOS device’s wifi" 181 | print u"Outputs to a file or to stdout" 182 | print u"Usage:" 183 | print u" {0:s} [-h] []".format(progname) 184 | 185 | 186 | def cli_main(): 187 | sys.stdout=SafeUnbuffered(sys.stdout) 188 | sys.stderr=SafeUnbuffered(sys.stderr) 189 | argv=unicode_argv() 190 | progname = os.path.basename(argv[0]) 191 | print u"{0} v{1}\nCopyright © 2013 Apprentice Alf".format(progname,__version__) 192 | 193 | try: 194 | opts, args = getopt.getopt(argv[1:], "h") 195 | except getopt.GetoptError, err: 196 | print u"Error in options or arguments: {0}".format(err.args[0]) 197 | usage(progname) 198 | sys.exit(2) 199 | 200 | for o, a in opts: 201 | if o == "-h": 202 | usage(progname) 203 | sys.exit(0) 204 | 205 | 206 | if len(args) < 2 or len(args) > 3: 207 | usage(progname) 208 | sys.exit(2) 209 | 210 | if len(args) == 3: 211 | # save to the specified file or folder 212 | getkey(args[0],args[1],args[2]) 213 | else: 214 | keys = generatekeys(args[0],args[1]) 215 | for key in keys: 216 | print key 217 | 218 | return 0 219 | 220 | 221 | def gui_main(): 222 | try: 223 | import Tkinter 224 | import Tkconstants 225 | import tkMessageBox 226 | except: 227 | print "Tkinter not installed" 228 | return cli_main() 229 | 230 | class DecryptionDialog(Tkinter.Frame): 231 | def __init__(self, root): 232 | Tkinter.Frame.__init__(self, root, border=5) 233 | self.status = Tkinter.Label(self, text=u"Enter parameters") 234 | self.status.pack(fill=Tkconstants.X, expand=1) 235 | body = Tkinter.Frame(self) 236 | body.pack(fill=Tkconstants.X, expand=1) 237 | sticky = Tkconstants.E + Tkconstants.W 238 | body.grid_columnconfigure(1, weight=2) 239 | Tkinter.Label(body, text=u"Amazon email address").grid(row=0) 240 | self.email = Tkinter.Entry(body, width=40) 241 | self.email.grid(row=0, column=1, sticky=sticky) 242 | Tkinter.Label(body, text=u"iOS MAC address").grid(row=1) 243 | self.mac = Tkinter.Entry(body, width=40) 244 | self.mac.grid(row=1, column=1, sticky=sticky) 245 | buttons = Tkinter.Frame(self) 246 | buttons.pack() 247 | button = Tkinter.Button( 248 | buttons, text=u"Generate", width=10, command=self.generate) 249 | button.pack(side=Tkconstants.LEFT) 250 | Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) 251 | button = Tkinter.Button( 252 | buttons, text=u"Quit", width=10, command=self.quit) 253 | button.pack(side=Tkconstants.RIGHT) 254 | 255 | def generate(self): 256 | email = self.email.get() 257 | mac = self.mac.get() 258 | if not email: 259 | self.status['text'] = u"Email not specified" 260 | return 261 | if not mac: 262 | self.status['text'] = u"MAC not specified" 263 | return 264 | self.status['text'] = u"Generating..." 265 | try: 266 | keys = generatekeys(email, mac) 267 | except Exception, e: 268 | self.status['text'] = u"Error: (0}".format(e.args[0]) 269 | return 270 | self.status['text'] = ", ".join(key for key in keys) 271 | 272 | root = Tkinter.Tk() 273 | root.title(u"Kindle for iOS PID Generator v.{0}".format(__version__)) 274 | root.resizable(True, False) 275 | root.minsize(300, 0) 276 | DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) 277 | root.mainloop() 278 | return 0 279 | 280 | if __name__ == '__main__': 281 | if len(sys.argv) > 1: 282 | sys.exit(cli_main()) 283 | sys.exit(gui_main()) 284 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/A_Patching_Experience.txt: -------------------------------------------------------------------------------- 1 | Of Historical Interest Only 2 | =========================== 3 | 4 | It is now much simpler and easier to get a backup.ab file from your Android device and import that into the tools. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Comment at Apprentice Alf's Blog by cestmoicestmoi, 21st December, 2012. 13 | ======================================================================== 14 | 15 | just to share my experience in patching the kindle.apk 16 | 17 | first thanks to all of you guys for this great site & tools, keep going 18 | 19 | following instructions in latest readme.txt 20 | successfully patched Android Kindle 3.7.0.108, Android 2.3.4 Xperia Arc unrooted; 21 | key painful snags encountered in my case (+ had never coded java/built any apk! .apk = app on Android) (just sums up here the tip of the iceberg): 22 | on phone : 23 | to get the app kindle.apk (invisible in unrooted phones) : 24 | use MyAppShare (free), but this required old Kindle.apk version (for patching) won’t probably be in your phone anymore (latest today 3.8.1.11, with new updates almost everyday !) 25 | anyway in any-case turnoff the Play-Store auto update 26 | otherwise/then look for a 3.7.0.108 on the web (or try patch a newer one ?) 27 | reminder: .apk are just containers = zip files… to check just add an extra .zip and unzip kindle.apk, but don’t play with that… patching wouldn’t work; however all names of kindle.apk can be completely freely changed : kindle2patchedJojo.apk, com.amazon.kindle.apk, etc. 28 | on your PC (XP): 29 | rather long & tricky expert process for any non-android-app maker 30 | (basics is to use the cmd.exe very old DOS style command line with the tricky path and directories changes, forward and back slash etc etc !) 31 | pb to obtain all the necessary .exe tools (then you call them as a ‘command’ in cdm.exe) 32 | to make things easier, put copy of everything in same folder anywhere: 33 | kindle.apk, cmd.exe (from XP), apktool.jar+apktool.bat+aapt.exe (from http://code.google.com/p/android-apktool/), patch.exe (from http://gnuwin32.sourceforge.net/packages/patch.htm), keytool.exe+jarsign.exe(from http://www.oracle.com/technetwork/java/javase/downloads/ download JDK+JRE same recent version, in my case v6038; start with keytool then jarsign; keep these .exe in their java/…/bin files, make shortcuts, copy them in your “work” folder, then do the path thing in cmd.exe in just copying the paths in the properties of the shortcuts) 34 | then, key points: 35 | download/use apktool.jar v143 not v150 (version seen in calling from cdm.exe) 36 | do not “apktool if…” (frameworks) 37 | besides all cmd.exe commands given in the readme.txt (read both .txt the old and the newer, even if same name) are perfectly ok (except in keytool: a typo: -valkeidity is -validity; and -genkey preferably -genkeypair) 38 | (for the tricky keytool then jarsign stuff better read for ex. https://www.owasp.org/index.php/Signing_jar_files_with_jarsigner) 39 | (almost1) finally move your patched & signed new kindle.apk anywhere to phone sd card, and then from within android just like possibly for any .apk move to it with any file manager and click it, it installs by itself (mine didn’t want to install over the non-patched kindle app, so I had to desinstall the latter 1st from within the settings, but it’s probably because my phone ram was full !) 40 | (almost2) finally you get your famous PIDs at the bottom of the info in the kindle.apk: don’t worry, in my test case (just 1 book yet), there were 11 PIDs, but 5 were redundant, and the last is a weird large neg number -obviously not a 10 max char. PID, so forget it-). 41 | then thanks to Android 2.3.4 now you can screenshot (press power button then back button), then from my PC i got this screen pict with all these PIDs, which I even OCRised (stupidly without checking… so I mixed up some “O” and “0″ etc !), 42 | (almost3) finally then I implemented these PIDs in deDRM v541, …and it didn’t work, long error message, but, finally, at last, worked in deDRM v531 ! 43 | 44 | conclusion: 45 | the idea and work behind this patch is brilliant… but I have the bad feeling that most of this/my rather crazy work above to do myself the whole patching thing was more for glory, like climbing some kind of Everest without any training, than anything else… 46 | must precise that I have only an old mac-ppc + this Android phone; ppc has no kindle.app available*; ppc has no java 1.6 so no possible apktool etc. (nor eclipse Android plugin etc.) (possible with very old versions ?), luckily I had an old version of VPC(virtualpc)/XP (extremely slow!), so I could try all this java stuff at least in a normal XP window/virtual machine, on my mac-ppc; i didn’t try any XP-Android link through USB or else, had only my usual Mac-Android wifi ftp (great direct drag’n drop with cyberduck). 47 | But when i see all these/my efforts described above for a non-specialist (like 1 week work vs. 1-2 hours for you apk developers !), I wonder if this patched apk shouldn’t/couldn’t be made directly available for most other average joes, no ? just like deDRM ? (or maybe i don’t see the problems here ?); 48 | besides, i didn’t check, but i have the strong feeling that in using kindle directly on XP to buy my books, this great app deDRM would have found directly these PIDs or other keys automatically (as it does for me on my mac-ppc for some ebook purchased and downloaded directly from other vendors; i.e. “stupid me”… on XP, the deDRM app would have worked in 1 sec… vs. 1 week work above for patching Android, + the crazy procedure just to get these pids, screenshot + ocr + etc. ! well, it should be much faster now, of course !). 49 | 50 | *reminder: there is a solution for a mac-ppc only guy who want to buy & read Kindle books for very cheap, and accept to read them online in a browser : little advertised “Kindle cloud reader” which used to work even with rather old Safari versions (didn’t check recently if it still works)… (looks a lot like the ‘Google play’ book reader) texts are very little protected/not encrypted there apparently (journalists even said at launch 2-3 years ago that Amazon was abandoning DRMs, just like Apple did with music a while ago) : texts can more or less easily be copied piece by piece/ by page with a few astute clicks (same for Google; but formatting is gone; and who want to recopy a 300 p. book page by page !); tried to reverse engineer a bit for fun this browser reader to see where and how the text was stored, etc. but (as mentioned I think by another in another comment) the text is downloaded only by pieces, not all at once (don’t remember what if browser turned “offline” ?), and stored here and there, in caches… maybe there are tools for capturing auto these books sent to browsers ? didn’t check ? (not for my mac-ppc at the time in any case; but possibly Windows). 51 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_3.0.1.70/ReadMe_K4Android.txt: -------------------------------------------------------------------------------- 1 | Kindle for Android 2 | ------------------ 3 | 4 | Kindle for Android uses a different scheme to generate its books specific PIDs than Kindle for PC, Kindle for iPhone/iPad, and standalone Kindles. 5 | 6 | Unfortunately, K4Android uses an "account secrets" file that would only be available if the device were jail-broken and even then someone would have to figure out how to decode this secret information in order to reverse the process. 7 | 8 | Instead of trying to calculate the correct PIDs for each book from this primary data, "Me" (a commenter who posted to the ApprenticeAlf site) came up with a wonderful idea to simply modify the Kindle 3 for Android application to store the PIDs it uses to decode each book in its "about activity" window. This list of PIDS can then be provided to MobiDeDRM.py, which in its latest incarnations allows a comma separated list of pids to be passed in, to successfully remove the DRM from that book. Effectively "Me" has created an "Unswindle" for the Kindle for Android 3 application! 9 | 10 | Obviously, to use "Me"'s approach, requires an Android Developer's Certificate (to sign the modified application) and access to and knowledge of the developer tools, but does not require anything to be jail-broken. 11 | 12 | This is a copy the detailed instructions supplied by "Me" to the ApprenticeAlf blog in the comments. The kindle3.patch described below is included in this folder in the tools: 13 | 14 | From the ApprenticeAlf Comments: 15 | 16 | "Me" writes: 17 | 18 | A better solution seems to create a patched version of the Kindle apk which either logs or displays it’s PID. I created a patch to both log the pid list and show it in the Kindle application in the about activity screen. The pid list isn’t available until the DRMed book has been opened (and the list seem to differ for different books). 19 | 20 | To create the patched kindle apk a certificate must be created (http://developer.android.com/guide/publishing/app-signing.html#cert) and the apktool must be build from source (all subprojects) as long as version 1.4.2 isn’t released (http://code.google.com/p/android-apktool/wiki/BuildApktool). 21 | 22 | These are the steps to pull the original apk from the Android device, uninstall it, create a patched apk and install that (tested on a rooted device, but I think all steps should also work on non-rooted devices): 23 | 24 | adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk 25 | adb uninstall com.amazon.kindle 26 | apktool d kindle3.apk kindle3 27 | cd kindle3 28 | patch -p1 < ../kindle3.patch 29 | cd .. 30 | apktool b kindle3 kindle3_patched.apk 31 | jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle 32 | zipalign -v 4 kindle3_patched.apk kindle3_signed.apk 33 | adb install kindle3_signed.apk 34 | 35 | kindle3.patch (based on kindle version 3.0.1.70) is available on pastebin: 36 | http://pastebin.com/LNpgkcpP 37 | 38 | Have fun! 39 | 40 | Comment by me — June 9, 2011 @ 9:01 pm | Reply 41 | 42 | Hi me, 43 | Wow! Great work!!!! 44 | 45 | With your patch, you have created the equivalent of Unswindle for the Kindle for Android app and it does not even require jailbreaking! 46 | 47 | Very nice work indeed! 48 | 49 | Comment by some_updates — June 10, 2011 @ 4:28 am | Reply 50 | 51 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_3.0.1.70/kindle3.0.1.70.patch: -------------------------------------------------------------------------------- 1 | diff -ru kindle3_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle3/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2 | --- kindle3_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 3 | +++ kindle3/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 4 | @@ -11,6 +11,8 @@ 5 | 6 | .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 7 | 8 | +.field private pidList:Ljava/lang/String; 9 | + 10 | 11 | # direct methods 12 | .method public constructor (Lcom/mobipocket/android/library/reader/AndroidSecurity;Lcom/amazon/kcp/application/AndroidDeviceType;)V 13 | @@ -28,6 +30,10 @@ 14 | .line 26 15 | iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AndroidDeviceType; 16 | 17 | + const-string v0, "Open DRMed book to show PID list." 18 | + 19 | + iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 20 | + 21 | .line 27 22 | new-instance v0, Ljava/lang/StringBuilder; 23 | 24 | @@ -175,4 +181,26 @@ 25 | move-result-object v0 26 | 27 | return-object v0 28 | +.end method 29 | + 30 | +.method public getPidList()Ljava/lang/String; 31 | + .locals 1 32 | + 33 | + .prologue 34 | + .line 15 35 | + iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 36 | + 37 | + return-object v0 38 | +.end method 39 | + 40 | +.method public setPidList(Ljava/lang/String;)V 41 | + .locals 0 42 | + .parameter "value" 43 | + 44 | + .prologue 45 | + .line 11 46 | + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 47 | + 48 | + .line 12 49 | + return-void 50 | .end method 51 | diff -ru kindle3_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle3/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 52 | --- kindle3_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 53 | +++ kindle3/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 54 | @@ -27,3 +27,9 @@ 55 | 56 | .method public abstract getPid()Ljava/lang/String; 57 | .end method 58 | + 59 | +.method public abstract getPidList()Ljava/lang/String; 60 | +.end method 61 | + 62 | +.method public abstract setPidList(Ljava/lang/String;)V 63 | +.end method 64 | \ No newline at end of file 65 | diff -ru kindle3_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle3/smali/com/amazon/kcp/info/AboutActivity.smali 66 | --- kindle3_orig/smali/com/amazon/kcp/info/AboutActivity.smali 67 | +++ kindle3/smali/com/amazon/kcp/info/AboutActivity.smali 68 | @@ -32,9 +32,11 @@ 69 | invoke-direct {v6, v1}, Ljava/util/ArrayList;->(I)V 70 | 71 | .line 36 72 | - const v1, 0x7f0b0005 73 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 74 | 75 | - invoke-virtual {p0, v1}, Lcom/amazon/kcp/info/AboutActivity;->getString(I)Ljava/lang/String; 76 | + move-result-object v0 77 | + 78 | + invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; 79 | 80 | move-result-object v1 81 | 82 | diff -ru kindle3_orig/smali/com/amazon/system/security/Security.smali kindle3/smali/com/amazon/system/security/Security.smali 83 | --- kindle3_orig/smali/com/amazon/system/security/Security.smali 84 | +++ kindle3/smali/com/amazon/system/security/Security.smali 85 | @@ -884,6 +884,15 @@ 86 | 87 | .line 332 88 | :cond_1 89 | + 90 | + const-string v1, "PID list" 91 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 92 | + move-result-object v0 93 | + invoke-static {v7}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; 94 | + move-result-object v2 95 | + invoke-interface {v0, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V 96 | + invoke-static {v1, v2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I 97 | + 98 | return-object v7 99 | 100 | :cond_2 101 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_3.7.0.108/ReadMe_K4Android.txt: -------------------------------------------------------------------------------- 1 | Kindle for Android 2 | ------------------ 3 | 4 | Kindle for Android uses a different scheme to generate its books specific PIDs than Kindle for PC, Kindle for iPhone/iPad, and standalone Kindles. 5 | 6 | Unfortunately, K4Android uses an "account secrets" file that would only be available if the device were jail-broken and even then someone would have to figure out how to decode this secret information in order to reverse the process. 7 | 8 | Instead of trying to calculate the correct PIDs for each book from this primary data, "Me" (a commenter who posted to the ApprenticeAlf site) came up with a wonderful idea to simply modify the Kindle 3 for Android application to store the PIDs it uses to decode each book in its "about activity" window. This list of PIDS can then be provided to MobiDeDRM.py, which in its latest incarnations allows a comma separated list of pids to be passed in, to successfully remove the DRM from that book. Effectively "Me" has created an "Unswindle" for the Kindle for Android 3 application! 9 | 10 | "Me"'s original patch was for Kindle for Android version 3.0.1.70. Now "Me II" has created a patch for Kindle for Android version 3.7.0.108 and new instructions, since some of the previous steps are no longer necessary. 11 | 12 | From the ApprenticeAlf Comments: 13 | 14 | 15 | "Me II" writes: 16 | 17 | Since “Me”‘s old method for getting PIDs from Kindle for Android is outdated and no longer works with newer versions of the app, I decided I’d take a stab at bringing it up to date. It took a little fiddling to get everything working, considering how much has changed since the last patch, but I managed to do it. The process is pretty much identical to “Me”‘s original instructions, with a few minor changes. 18 | 19 | 1) You don’t need to build apktool from source. You can just grab the binaries from here for whatever OS you’re running: http://code.google.com/p/android-apktool/ 20 | 2) When you sign the rebuilt APK, use the following command instead of the one in the instructions: 21 | jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore kindle.keystore kindle3_patched.apk kindle 22 | 3) It no longer logs the PIDs, only displays them within the app. 23 | 24 | You can get the new patch, for version 3.7.0.108, here: http://pastebin.com/6FN2cTSN 25 | 26 | And here’s a screenshot of the updated menu: http://imgur.com/BbFVF (sorry for the Japanese, I was too lazy to change my phone’s language). 27 | 28 | Subsequently, "s" wrote: 29 | 30 | For others it would be useful to add the keystore generation command into the help file: 31 | keytool -genkey -v -keystore kindle.keystore -alias kindle -keyalg RSA -keysize 2048 -validity 10000 32 | As well as location of prc’s on android being (with sdcard): 33 | /mnt/sdcard/Android/data/com.amazon.kindle/files/ 34 | 35 | "s" also reported success with using the patch on version 3.7.1.8, although I recommend using the 3.7.0.108 version just in case. 36 | 37 | 38 | "Me"'s original instructions, from the ApprenticeAlf Comments: 39 | 40 | "Me" writes: 41 | 42 | A better solution seems to create a patched version of the Kindle apk which either logs or displays it’s PID. I created a patch to both log the pid list and show it in the Kindle application in the about activity screen. The pid list isn’t available until the DRMed book has been opened (and the list seem to differ for different books). 43 | 44 | To create the patched kindle apk a certificate must be created (http://developer.android.com/guide/publishing/app-signing.html#cert) and the apktool must be build from source (all subprojects) as long as version 1.4.2 isn’t released (http://code.google.com/p/android-apktool/wiki/BuildApktool). 45 | 46 | These are the steps to pull the original apk from the Android device, uninstall it, create a patched apk and install that (tested on a rooted device, but I think all steps should also work on non-rooted devices): 47 | 48 | adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk 49 | adb uninstall com.amazon.kindle 50 | apktool d kindle3.apk kindle3 51 | cd kindle3 52 | patch -p1 < ../kindle3.patch 53 | cd .. 54 | apktool b kindle3 kindle3_patched.apk 55 | jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle 56 | zipalign -v 4 kindle3_patched.apk kindle3_signed.apk 57 | adb install kindle3_signed.apk 58 | 59 | kindle3.patch (based on kindle version 3.0.1.70) is available on pastebin: 60 | http://pastebin.com/LNpgkcpP 61 | 62 | Have fun! 63 | 64 | Comment by me — June 9, 2011 @ 9:01 pm | Reply 65 | 66 | Hi me, 67 | Wow! Great work!!!! 68 | 69 | With your patch, you have created the equivalent of Unswindle for the Kindle for Android app and it does not even require jailbreaking! 70 | 71 | Very nice work indeed! 72 | 73 | Comment by some_updates — June 10, 2011 @ 4:28 am | Reply 74 | 75 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_3.7.0.108/kindle3.7.0.108.patch: -------------------------------------------------------------------------------- 1 | diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2 | --- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 3 | +++ kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 4 | @@ -43,6 +43,8 @@ 5 | 6 | .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 7 | 8 | +.field private pidList:Ljava/lang/String; 9 | + 10 | .field private totalMemory:J 11 | 12 | 13 | @@ -78,6 +80,10 @@ 14 | 15 | .line 132 16 | iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType; 17 | + 18 | + const-string v0, "Open DRMed book to show PID list." 19 | + 20 | + iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 21 | 22 | .line 133 23 | sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String; 24 | @@ -1242,3 +1248,25 @@ 25 | 26 | return-wide v0 27 | .end method 28 | + 29 | +.method public getPidList()Ljava/lang/String; 30 | + .locals 1 31 | + 32 | + .prologue 33 | + .line 15 34 | + iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 35 | + 36 | + return-object v0 37 | +.end method 38 | + 39 | +.method public setPidList(Ljava/lang/String;)V 40 | + .locals 0 41 | + .parameter "value" 42 | + 43 | + .prologue 44 | + .line 11 45 | + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 46 | + 47 | + .line 12 48 | + return-void 49 | +.end method 50 | \ No newline at end of file 51 | diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 52 | --- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 53 | +++ kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 54 | @@ -30,3 +30,9 @@ 55 | 56 | .method public abstract getPid()Ljava/lang/String; 57 | .end method 58 | + 59 | +.method public abstract getPidList()Ljava/lang/String; 60 | +.end method 61 | + 62 | +.method public abstract setPidList(Ljava/lang/String;)V 63 | +.end method 64 | \ No newline at end of file 65 | diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali 66 | --- kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali 67 | +++ kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali 68 | @@ -493,6 +493,57 @@ 69 | return-void 70 | .end method 71 | 72 | +.method private populatePIDList()V 73 | + .locals 7 74 | + 75 | + .prologue 76 | + .line 313 77 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 78 | + 79 | + move-result-object v0 80 | + 81 | + invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; 82 | + 83 | + move-result-object v1 84 | + 85 | + .line 314 86 | + .local v1, PidList:Ljava/lang/String; 87 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; 88 | + 89 | + new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem; 90 | + 91 | + const-string v5, "PID List" 92 | + 93 | + const v6, 0x1 94 | + 95 | + invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V 96 | + 97 | + invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z 98 | + 99 | + .line 315 100 | + new-instance v2, Ljava/util/ArrayList; 101 | + 102 | + invoke-direct {v2}, Ljava/util/ArrayList;->()V 103 | + 104 | + .line 316 105 | + .local v2, children:Ljava/util/List;,"Ljava/util/List;" 106 | + new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem; 107 | + 108 | + const-string v4, "PIDs" 109 | + 110 | + invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V 111 | + 112 | + invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z 113 | + 114 | + .line 317 115 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; 116 | + 117 | + invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z 118 | + 119 | + .line 318 120 | + return-void 121 | +.end method 122 | + 123 | .method private populateDisplayItems()V 124 | .locals 1 125 | 126 | @@ -539,6 +590,9 @@ 127 | invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V 128 | 129 | .line 190 130 | + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V 131 | + 132 | + .line 191 133 | return-void 134 | 135 | .line 172 136 | diff -ru kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali kindle3.7.0.108/smali/com/amazon/system/security/Security.smali 137 | --- kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali 138 | +++ kindle3.7.0.108/smali/com/amazon/system/security/Security.smali 139 | @@ -926,6 +926,16 @@ 140 | sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String; 141 | 142 | aput-object v0, v6, v8 143 | + 144 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 145 | + 146 | + move-result-object v5 147 | + 148 | + invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; 149 | + 150 | + move-result-object v2 151 | + 152 | + invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V 153 | 154 | .line 353 155 | return-object v6 156 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch: -------------------------------------------------------------------------------- 1 | Only in kindle4.0.2.1: build 2 | diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 3 | --- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 4 | +++ kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-23 16:54:53.000000000 -0500 5 | @@ -36,20 +36,22 @@ 6 | .field private maxCpuSpeed:J 7 | 8 | .field private maxMemory:J 9 | 10 | .field private minCpuSpeed:J 11 | 12 | .field private resources:Landroid/content/res/Resources; 13 | 14 | .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 15 | 16 | +.field private pidList:Ljava/lang/String; 17 | + 18 | .field private totalMemory:J 19 | 20 | 21 | # direct methods 22 | .method static constructor ()V 23 | .locals 1 24 | 25 | .prologue 26 | .line 30 27 | const-class v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider; 28 | @@ -72,20 +74,24 @@ 29 | .prologue 30 | .line 130 31 | invoke-direct {p0}, Ljava/lang/Object;->()V 32 | 33 | .line 131 34 | iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 35 | 36 | .line 132 37 | iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType; 38 | 39 | + const-string v0, "Open DRMed book to show PID list." 40 | + 41 | + iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 42 | + 43 | .line 133 44 | sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String; 45 | 46 | new-instance v0, Ljava/lang/StringBuilder; 47 | 48 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 49 | 50 | const-string v1, "Device Type is set to \"" 51 | 52 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 53 | @@ -1235,10 +1241,33 @@ 54 | move-result-wide v0 55 | 56 | iput-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J 57 | 58 | .line 308 59 | :cond_0 60 | iget-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J 61 | 62 | return-wide v0 63 | .end method 64 | + 65 | +.method public getPidList()Ljava/lang/String; 66 | + .locals 1 67 | + 68 | + .prologue 69 | + .line 15 70 | + iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 71 | + 72 | + return-object v0 73 | +.end method 74 | + 75 | +.method public setPidList(Ljava/lang/String;)V 76 | + .locals 0 77 | + .parameter "value" 78 | + 79 | + .prologue 80 | + .line 11 81 | + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 82 | + 83 | + .line 12 84 | + return-void 85 | +.end method 86 | + 87 | diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 88 | --- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 89 | +++ kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-23 16:55:58.000000000 -0500 90 | @@ -23,10 +23,16 @@ 91 | .end method 92 | 93 | .method public abstract getDeviceTypeId()Ljava/lang/String; 94 | .end method 95 | 96 | .method public abstract getOsVersion()Ljava/lang/String; 97 | .end method 98 | 99 | .method public abstract getPid()Ljava/lang/String; 100 | .end method 101 | + 102 | +.method public abstract getPidList()Ljava/lang/String; 103 | +.end method 104 | + 105 | +.method public abstract setPidList(Ljava/lang/String;)V 106 | +.end method 107 | diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali 108 | --- kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-22 18:39:03.000000000 -0500 109 | +++ kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-23 17:18:14.000000000 -0500 110 | @@ -486,20 +486,71 @@ 111 | .end local v2 #screenDpi:Ljava/lang/String; 112 | :cond_0 113 | iget-object v5, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; 114 | 115 | invoke-interface {v5, v0}, Ljava/util/List;->add(Ljava/lang/Object;)Z 116 | 117 | .line 317 118 | return-void 119 | .end method 120 | 121 | +.method private populatePIDList()V 122 | + .locals 7 123 | + 124 | + .prologue 125 | + .line 313 126 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 127 | + 128 | + move-result-object v0 129 | + 130 | + invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; 131 | + 132 | + move-result-object v1 133 | + 134 | + .line 314 135 | + .local v1, PidList:Ljava/lang/String; 136 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; 137 | + 138 | + new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem; 139 | + 140 | + const-string v5, "PID List" 141 | + 142 | + const v6, 0x1 143 | + 144 | + invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V 145 | + 146 | + invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z 147 | + 148 | + .line 315 149 | + new-instance v2, Ljava/util/ArrayList; 150 | + 151 | + invoke-direct {v2}, Ljava/util/ArrayList;->()V 152 | + 153 | + .line 316 154 | + .local v2, children:Ljava/util/List;,"Ljava/util/List;" 155 | + new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem; 156 | + 157 | + const-string v4, "PIDs" 158 | + 159 | + invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V 160 | + 161 | + invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z 162 | + 163 | + .line 317 164 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; 165 | + 166 | + invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z 167 | + 168 | + .line 318 169 | + return-void 170 | +.end method 171 | + 172 | .method private populateDisplayItems()V 173 | .locals 1 174 | 175 | .prologue 176 | .line 171 177 | iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; 178 | 179 | if-nez v0, :cond_0 180 | 181 | .line 173 182 | @@ -531,20 +582,22 @@ 183 | 184 | .line 192 185 | invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateRamInformation()V 186 | 187 | .line 193 188 | invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateStorageInformation()V 189 | 190 | .line 194 191 | invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V 192 | 193 | + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V 194 | + 195 | .line 195 196 | return-void 197 | 198 | .line 177 199 | :cond_0 200 | iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; 201 | 202 | invoke-interface {v0}, Ljava/util/List;->clear()V 203 | 204 | goto :goto_0 205 | diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali kindle4.0.2.1/smali/com/amazon/system/security/Security.smali 206 | --- kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali 2013-05-22 18:39:04.000000000 -0500 207 | +++ kindle4.0.2.1/smali/com/amazon/system/security/Security.smali 2013-05-23 17:19:05.000000000 -0500 208 | @@ -920,20 +920,30 @@ 209 | 210 | .line 350 211 | :cond_2 212 | add-int/lit8 v8, v8, 0x1 213 | 214 | .line 351 215 | sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String; 216 | 217 | aput-object v0, v6, v8 218 | 219 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 220 | + 221 | + move-result-object v5 222 | + 223 | + invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; 224 | + 225 | + move-result-object v2 226 | + 227 | + invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V 228 | + 229 | .line 353 230 | return-object v6 231 | .end method 232 | 233 | 234 | # virtual methods 235 | .method public customDrmOnly()I 236 | .locals 1 237 | 238 | .prologue 239 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_4.8.1.10/Notes on the Patch.txt: -------------------------------------------------------------------------------- 1 | Notes from UE01 about this patch: 2 | 3 | 1. Revised the “Other_Tools/Kindle_for_Android_Patches/” for Kindle 4.8.1.10 4 | 2. Built Kindle 4.8.1.10 with the PID List added to the About activity 5 | 3. Uninstalled the Amazon/Play-store version and installed the patched version 6 | 4. Signed in to Amazon 7 | 5. Opened the book 8 | 6. Did Info > About > PID List and copied the PIDs to Calibre’s Plugins>File type > DeDRM > Mobipocket dialog 9 | 7. **Crucial** copied the PRC file to the PC (because the file’s checksum has changed since it was last copied) 10 | 8. In Calibre, Add Books (from a single directory) 11 | 12 | -------------------------------------------------------------------------------- /Other_Tools/Kindle_for_Android_Patches/kindle_version_4.8.1.10/kindle4.8.1.10.patch: -------------------------------------------------------------------------------- 1 | diff --git a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2 | index 8ea400e..3aefad2 100644 3 | --- a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 4 | +++ b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 5 | @@ -41,6 +41,8 @@ 6 | 7 | .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 8 | 9 | +.field private pidList:Ljava/lang/String; 10 | + 11 | .field private totalMemory:J 12 | 13 | 14 | @@ -74,6 +76,10 @@ 15 | .line 133 16 | iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity; 17 | 18 | + const-string v0, "Open DRMed book to show PID list." 19 | + 20 | + iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 21 | + 22 | .line 134 23 | sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String; 24 | 25 | @@ -1339,3 +1345,26 @@ 26 | 27 | return-wide v0 28 | .end method 29 | + 30 | +.method public getPidList()Ljava/lang/String; 31 | + .locals 1 32 | + 33 | + .prologue 34 | + .line 15 35 | + iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 36 | + 37 | + return-object v0 38 | +.end method 39 | + 40 | +.method public setPidList(Ljava/lang/String;)V 41 | + .locals 0 42 | + .param p1, "value" 43 | + 44 | + .prologue 45 | + .line 11 46 | + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; 47 | + 48 | + .line 12 49 | + return-void 50 | +.end method 51 | + 52 | diff --git a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 53 | index e4a3523..2269fab 100644 54 | --- a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 55 | +++ b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 56 | @@ -30,3 +30,9 @@ 57 | 58 | .method public abstract getPid()Ljava/lang/String; 59 | .end method 60 | + 61 | +.method public abstract getPidList()Ljava/lang/String; 62 | +.end method 63 | + 64 | +.method public abstract setPidList(Ljava/lang/String;)V 65 | +.end method 66 | diff --git a/smali/com/amazon/kcp/info/AboutActivity.smali b/smali/com/amazon/kcp/info/AboutActivity.smali 67 | index 5640e9e..e298341 100644 68 | --- a/smali/com/amazon/kcp/info/AboutActivity.smali 69 | +++ b/smali/com/amazon/kcp/info/AboutActivity.smali 70 | @@ -493,6 +493,57 @@ 71 | return-void 72 | .end method 73 | 74 | +.method private populatePIDList()V 75 | + .locals 7 76 | + 77 | + .prologue 78 | + .line 313 79 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 80 | + 81 | + move-result-object v0 82 | + 83 | + invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; 84 | + 85 | + move-result-object v1 86 | + 87 | + .line 314 88 | + .local v1, "PidList":Ljava/lang/String; 89 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; 90 | + 91 | + new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem; 92 | + 93 | + const-string v5, "PID List" 94 | + 95 | + const v6, 0x1 96 | + 97 | + invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V 98 | + 99 | + invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z 100 | + 101 | + .line 315 102 | + new-instance v2, Ljava/util/ArrayList; 103 | + 104 | + invoke-direct {v2}, Ljava/util/ArrayList;->()V 105 | + 106 | + .line 316 107 | + .local v2, "children":Ljava/util/List;,"Ljava/util/List;" 108 | + new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem; 109 | + 110 | + const-string v4, "PIDs" 111 | + 112 | + invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V 113 | + 114 | + invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z 115 | + 116 | + .line 317 117 | + iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; 118 | + 119 | + invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z 120 | + 121 | + .line 318 122 | + return-void 123 | +.end method 124 | + 125 | .method private populateDisplayItems()V 126 | .locals 1 127 | 128 | @@ -538,6 +589,8 @@ 129 | .line 173 130 | invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V 131 | 132 | + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V 133 | + 134 | .line 174 135 | return-void 136 | 137 | diff --git a/smali/com/amazon/system/security/Security.smali b/smali/com/amazon/system/security/Security.smali 138 | index 04ea997..e88fe08 100644 139 | --- a/smali/com/amazon/system/security/Security.smali 140 | +++ b/smali/com/amazon/system/security/Security.smali 141 | @@ -940,6 +940,16 @@ 142 | 143 | aput-object v0, v6, v8 144 | 145 | + invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; 146 | + 147 | + move-result-object v5 148 | + 149 | + invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; 150 | + 151 | + move-result-object v2 152 | + 153 | + invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V 154 | + 155 | .line 347 156 | return-object v6 157 | .end method -------------------------------------------------------------------------------- /Other_Tools/Rocket_ebooks/rebhack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noDRM/DeDRM_tools/7379b453199ed1ba91bf3a4ce4875d5ed3c309a9/Other_Tools/Rocket_ebooks/rebhack.zip -------------------------------------------------------------------------------- /Other_Tools/Rocket_ebooks/rebhack_ReadMe.txt: -------------------------------------------------------------------------------- 1 | Rocket eBooks 2 | ============= 3 | 4 | Rocket ebooks (.rb) are no longer sold. 5 | 6 | This is the only archive of tools for decrypting Rocket ebooks that I have been able to find. It is included here without further comment or support. 7 | 8 | — Alf. 9 | -------------------------------------------------------------------------------- /Other_Tools/Scuolabook_DRM/Scuolabook_ReadMe.txt: -------------------------------------------------------------------------------- 1 | The latest Scuolabook tool can be found at Hex's own blog: 2 | https://thisishex.wordpress.com/scuolabook-drm-remover/ 3 | 4 | Harper. -------------------------------------------------------------------------------- /Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51_ReadMe.txt: -------------------------------------------------------------------------------- 1 | ineptpdf 8.4.51 2 | --------------- 3 | 4 | This is a version of the ineptpdf script produced by TetraChroma that can remove, on Windows, "FileOpen" DRM. 5 | 6 | No support for this script is offered at Apprentice Alf's blog. 7 | 8 | Trtrachroma's blog is http://tetrachroma.wordpress.com/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeDRM_tools 2 | DeDRM tools for ebooks 3 | 4 | This is a fork of Apprentice Harper's version of the DeDRM tools. Apprentice Harper said that the original version of the plugin [is no longer maintained](https://github.com/apprenticeharper/DeDRM_tools#no-longer-maintained), so I've taken over, merged a bunch of open PRs, and added a ton more features and bugfixes. 5 | 6 | The latest stable (released) version is v10.0.3 which [can be downloaded here](https://github.com/noDRM/DeDRM_tools/releases/tag/v10.0.3). The latest beta is v10.0.9, as a release candidate for v10.1.0. It [can be downloaded here](https://github.com/noDRM/DeDRM_tools/releases/tag/v10.0.9). 7 | 8 | The latest alpha version is available [at this link](https://github.com/noDRM/DeDRM_tools_autorelease/releases). This version is completely untested and will contain the latest code changes in this repository. With each commit in this repository, a new automatic alpha version will be uploaded there. If you want the most up-to-date code to test things and are okay with the plugin occasionally breaking, you can download this version. 9 | 10 | Take a look at [the CHANGELOG](https://github.com/noDRM/DeDRM_tools/blob/master/CHANGELOG.md) to see a list of changes since the last version by Apprentice Harper (v7.2.1). 11 | 12 | My version of the plugin should both work with Calibre 5.x/6.x (Python 3) as well as Calibre 4.x and lower (Python 2). If you encounter issues with this plugin in Calibre 4.x or lower, please open a bug report. 13 | 14 | # Original README from Apprentice Harper 15 | 16 | This is a repository that tracks all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This includes the tools from a time before Apprentice Alf had a blog, and continues through to when Apprentice Harper (with help) took over maintenance of the tools. 17 | 18 | The individual scripts are now released as two plugins for calibre: DeDRM and Obok. 19 | The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM, Barnes & Noble DRM, and some historical formats. 20 | The Obok plugin handles Kobo DRM. 21 | 22 | Users with calibre 5.x or later should use release 7.2.0 or later of the tools. 23 | Users with calibe 4.x or earlier should use release 6.8.x of the tools. 24 | 25 | For the latest Amazon KFX format, users of the calibre plugin should also install the KFX Input plugin from the standard calibre plugin menu. It's also available from the MobileRead thread here: https://www.mobileread.com/forums/showthread.php?t=291290 26 | 27 | Note that Amazon changes the DRM for KFX files frequently. What works for KFX today might not work tomorrow. 28 | 29 | I welcome contributions from others to improve these tools, from expanding the range of books handled, improving key retrieval, to just general bug fixes, speed improvements and UI enhancements. 30 | 31 | I urge people to read the FAQs. But to cover the most common: Do remember to unzip the downloaded archive to get the plugin (beta versions may be just the plugin don't unzip that). You can't load the whole tools archive into calibre. 32 | 33 | My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools. 34 | 35 | Apprentice Harper. 36 | -------------------------------------------------------------------------------- /ReadMe_Overview.txt: -------------------------------------------------------------------------------- 1 | Welcome to the tools! 2 | ===================== 3 | 4 | This file is to give users a quick overview of what is available and how to get started. This document is part of the DeDRM Tools archive from noDRM's github repository: https://github.com/noDRM/DeDRM_tools/ 5 | 6 | This archive includes calibre plugins to remove DRM from: 7 | 8 | - Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles). 9 | - Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE) 10 | - Adobe Digital Editions PDFs 11 | - Kobo kePubs from the Kobo Desktop application and attached Kobo readers. 12 | 13 | These tools do NOT work with Apple's iBooks FairPlay DRM. Use iBook Copy from TunesKit. 14 | These tools no longer work well with books from Barnes & Noble. 15 | Due to a DMCA request, these tools no longer work with LCP-encrypted books - see https://github.com/noDRM/DeDRM_tools/issues/18 for details. 16 | 17 | For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md 18 | 19 | About the tools 20 | --------------- 21 | These tools are updated and maintained by noDRM and many others. They are based on Apprentice Harper's Calibre plugin. You can find the latest updates at noDRM's github repository https://github.com/noDRM/DeDRM_tools/ and get support by creating an issue at the repository (github account required). 22 | 23 | If you re-post these tools, a link to the repository would be appreciated. 24 | 25 | The tools are provided in the form of plugins for calibre. Calibre is an open source freeware ebook library manager. It is the best tool around for keeping track of your ebooks. 26 | 27 | 28 | DeDRM plugin for calibre (Linux, Mac OS X and Windows) 29 | ------------------------------------------------------- 30 | calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. 31 | 32 | The DeDRM plugin for calibre removes DRM from your Kindle and Adobe DRM ebooks when they are imported to calibre. Just install the DeDRM plugin (DeDRM_plugin.zip), following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs. 33 | 34 | Once installed and configured, you can simply add a DRM book to calibre and a DRM-free version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time. If you have already imported DRMed books you'll need to remove the books from calibre and re-import them. 35 | 36 | 37 | Obok plugin for calibre (Mac OS X and Windows) 38 | ---------------------------------------------- 39 | To import ebooks from the Kobo Desktop app or from a Kobo ebook reader, install the Obok plugin. This works in a different way to the DeDRM plugin, in that it finds your ebooks downloaded using the Kobo Desktop app, or on an attached Kobo ebooks reader, and displays them in a list, so that you can choose the ones you want to import into calibre. 40 | 41 | For instructions, see the obok_plugin_ReadMe.txt file. 42 | 43 | 44 | Credits 45 | ------- 46 | The original inept and ignoble scripts were by i♥cabbages 47 | ~The original Readium LCP DRM removal by NoDRM~ (removed due to a DMCA request) 48 | The original mobidedrm and erdr2pml scripts were by The Dark Reverser 49 | The original topaz DRM removal script was by CMBDTC 50 | The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson 51 | The original KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997 52 | The alfcrypto library is by some_updates 53 | The DeDRM plugin is based on plugins by DiapDealer and is currently maintained by noDRM 54 | The DeDRM plugin has been maintained by Apprentice Alf and Apprentice Harper until 2021. 55 | 56 | The original obok script was by Physisticated 57 | The plugin conversion was done anonymously. 58 | The Kobo reader support was added by norbusan 59 | 60 | Fixes, updates and enhancements to the scripts and applicatons have been made by many other anonymous people. 61 | -------------------------------------------------------------------------------- /make_release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | A wrapper script to generate zip files for GitHub releases. 6 | 7 | This script tends to be compatible with both Python 2 and Python 3. 8 | ''' 9 | 10 | from __future__ import print_function 11 | 12 | import os 13 | import shutil 14 | 15 | 16 | DEDRM_SRC_DIR = 'DeDRM_plugin' 17 | DEDRM_SRC_TMP_DIR = 'DeDRM_plugin_temp' 18 | DEDRM_README= 'DeDRM_plugin_ReadMe.txt' 19 | OBOK_SRC_DIR = 'Obok_plugin' 20 | OBOK_README = 'obok_plugin_ReadMe.txt' 21 | RELEASE_DIR = 'release' 22 | 23 | def patch_file(filepath): 24 | f = open(filepath, "rb") 25 | fn = open(filepath + ".tmp", "wb") 26 | patch = open(os.path.join(DEDRM_SRC_DIR, "__calibre_compat_code.py"), "rb") 27 | patchdata = patch.read() 28 | patch.close() 29 | 30 | while True: 31 | line = f.readline() 32 | if len(line) == 0: 33 | break 34 | 35 | if line.strip().startswith(b"#@@CALIBRE_COMPAT_CODE@@"): 36 | fn.write(patchdata) 37 | else: 38 | fn.write(line) 39 | 40 | f.close() 41 | fn.close() 42 | shutil.move(filepath + ".tmp", filepath) 43 | 44 | 45 | 46 | def make_release(version): 47 | try: 48 | shutil.rmtree(RELEASE_DIR) 49 | except: 50 | pass 51 | try: 52 | shutil.rmtree(DEDRM_SRC_TMP_DIR) 53 | except: 54 | pass 55 | 56 | os.mkdir(RELEASE_DIR) 57 | 58 | # Copy folder 59 | shutil.copytree(DEDRM_SRC_DIR, DEDRM_SRC_TMP_DIR) 60 | 61 | # Modify folder 62 | try: 63 | shutil.rmtree(os.path.join(os.path.abspath(DEDRM_SRC_TMP_DIR), "__pycache__")) 64 | except: 65 | pass 66 | 67 | # Patch file to add compat code. 68 | for root, dirs, files in os.walk(DEDRM_SRC_TMP_DIR): 69 | for name in files: 70 | if name.endswith(".py"): 71 | patch_file(os.path.join(root, name)) 72 | 73 | 74 | # Package 75 | shutil.make_archive(DEDRM_SRC_DIR, 'zip', DEDRM_SRC_TMP_DIR) 76 | shutil.make_archive(OBOK_SRC_DIR, 'zip', OBOK_SRC_DIR) 77 | shutil.move(DEDRM_SRC_DIR+'.zip', RELEASE_DIR) 78 | shutil.move(OBOK_SRC_DIR+'.zip', RELEASE_DIR) 79 | shutil.copy(DEDRM_README, RELEASE_DIR) 80 | shutil.copy(OBOK_README, RELEASE_DIR) 81 | shutil.copy("ReadMe_Overview.txt", RELEASE_DIR) 82 | 83 | # Remove temp folder: 84 | shutil.rmtree(DEDRM_SRC_TMP_DIR) 85 | 86 | if version is not None: 87 | release_name = 'DeDRM_tools_{}'.format(version) 88 | else: 89 | release_name = 'DeDRM_tools' 90 | result = shutil.make_archive(release_name, 'zip', RELEASE_DIR) 91 | try: 92 | shutil.rmtree(RELEASE_DIR) 93 | except: 94 | pass 95 | return result 96 | 97 | 98 | if __name__ == '__main__': 99 | import sys 100 | try: 101 | version = sys.argv[1] 102 | except IndexError: 103 | version = None 104 | 105 | print(make_release(version)) 106 | -------------------------------------------------------------------------------- /obok_plugin_ReadMe.txt: -------------------------------------------------------------------------------- 1 | obok_plugin.zip 2 | ================ 3 | 4 | This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application, or from Kobo ebooks on an attached E-Ink Kobo reader (but not a Kobo Arc or Kobo Vox). If both are available, ebooks will be read from the attached E-Ink Kobo reader. To import from the desktop application, unplug the Kobo reader. 5 | 6 | 7 | Installation 8 | ------------ 9 | Open calibre's Preferences dialog. Click on the "Plugins" button. Next, click on the button, "Load plugin from file". Navigate to the unzipped DeDRM_tools folder, find the file "obok_plugin.zip". Click to select the file and select "Open". Click "Yes" in the "Are you sure?" dialog box. Click the "OK" button in the "Success" dialog box. 10 | 11 | Note: This plugin requires the "wmic" component on Windows. On Windows 10 and below this will be available by default, on Windows 11 it needs to be explicitly enabled. Make sure that on your Windows 11 machine, under Settings -> System -> Optional features -> Add an optional feature -> View features, "WMIC" is enabled / activated, otherwise this plugin may not work correctly. 12 | 13 | 14 | Customization 15 | ------------- 16 | No customization is required, except choosing which menus will show the plugin. Although the ability to enter a device serial number is given, this should not need to be filled in, as the serial number should be picked up automatically from the attached Kobo reader. 17 | 18 | 19 | Using the plugin 20 | ---------------- 21 | Select the plugin's menu or icon from whichever part of the calibre interface you have chosen to have it. Follow the instructions in the dialog that appears. 22 | 23 | 24 | Troubleshooting 25 | --------------- 26 | If you find that the DeDRM plugin is not working for you (imported ebooks still have DRM - that is, they won't convert or open in the calibre ebook viewer), you should make a log of the import process by deleting the DRMed ebook from calibre and then adding the ebook to calibre when it's running in debug mode. This will generate a lot of helpful debugging info that can be copied into any online help requests. Here's how to do it: 27 | 28 | - Remove the DRMed book from calibre. 29 | - Click the Preferences drop-down menu and choose 'Restart in debug mode'. 30 | - Once calibre has re-started, import the problem ebook. 31 | - Now close calibre. 32 | 33 | A log will appear that you can copy and paste into a GitHub issue at noDRM's repository, https://github.com/noDRM/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file. 34 | 35 | --------------------------------------------------------------------------------