mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-04-07 03:38:55 +00:00
Compare commits
87 Commits
f4634b5eab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7379b45319 | ||
|
|
bde82fd7ab | ||
|
|
de3d91f5e5 | ||
|
|
c5ee327a60 | ||
|
|
501a1e6d31 | ||
|
|
815d86efe0 | ||
|
|
65646f4493 | ||
|
|
808dc7d29a | ||
|
|
2cd2792306 | ||
|
|
2e53d70e88 | ||
|
|
05fff5217b | ||
|
|
34c4c067e8 | ||
|
|
195ea69537 | ||
|
|
3373d93874 | ||
|
|
bf2471e65b | ||
|
|
5492dcdbf4 | ||
|
|
737d5e7f1e | ||
|
|
e4e5808894 | ||
|
|
ef67dbd204 | ||
|
|
10b6caf9f5 | ||
|
|
53996cf49c | ||
|
|
d388ae72fd | ||
|
|
bc089ee46d | ||
|
|
e509b7d520 | ||
|
|
e82d2b5c9c | ||
|
|
7f6dd84389 | ||
|
|
b9bad26d4b | ||
|
|
2a1413297e | ||
|
|
815f880e34 | ||
|
|
9ae77c438f | ||
|
|
abc5de018e | ||
|
|
133e67fa03 | ||
|
|
f86cff285b | ||
|
|
a553a71f45 | ||
|
|
740b46546f | ||
|
|
fb8b003444 | ||
|
|
3c12806f38 | ||
|
|
3151dbbd98 | ||
|
|
08e7ac79ca | ||
|
|
a711954323 | ||
|
|
a30405bebf | ||
|
|
901a6c091d | ||
|
|
e16748e854 | ||
|
|
06df18bea3 | ||
|
|
06648eeb1c | ||
|
|
6c8051eded | ||
|
|
1cc245b103 | ||
|
|
eb45c71fd9 | ||
|
|
2d4c5d2c4b | ||
|
|
21281baf21 | ||
|
|
88b0966961 | ||
|
|
52cf3faa59 | ||
|
|
b12e567c5f | ||
|
|
ca6d30b2d9 | ||
|
|
dfa247bf88 | ||
|
|
a0bb84fbfc | ||
|
|
410e086d08 | ||
|
|
9276d77f63 | ||
|
|
de23b5c221 | ||
|
|
b404605878 | ||
|
|
1cc5d383cc | ||
|
|
41df9ecda0 | ||
|
|
80cbaa4841 | ||
|
|
9a11f480b5 | ||
|
|
59839ae5c7 | ||
|
|
c15135b12f | ||
|
|
077e8f5c2a | ||
|
|
fed8bb716b | ||
|
|
c12d214b59 | ||
|
|
012ff533ab | ||
|
|
dcbb377566 | ||
|
|
76ce6d9c5c | ||
|
|
726d72217e | ||
|
|
2d51005cf1 | ||
|
|
7eb8f07a33 | ||
|
|
e4fe032e47 | ||
|
|
bb170688ba | ||
|
|
b283777c0a | ||
|
|
cf095a4171 | ||
|
|
263cc1d2cf | ||
|
|
a4689f6ac0 | ||
|
|
82a698edf6 | ||
|
|
227bda1ea6 | ||
|
|
93ff0aac20 | ||
|
|
1f13ae0f78 | ||
|
|
c5aebcca01 | ||
|
|
a1dd63ae5f |
8
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
8
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
@@ -10,16 +10,16 @@ body:
|
|||||||
id: calibre-version
|
id: calibre-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Which version of Calibre are you running?
|
label: Which version of Calibre are you running?
|
||||||
description: "Example: 5.32"
|
description: "Example: 6.23"
|
||||||
placeholder: "5.32"
|
placeholder: "6.23"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: plugin-version
|
id: plugin-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Which version of the DeDRM plugin are you running?
|
label: Which version of the DeDRM plugin are you running?
|
||||||
description: "Example: v10.0.0"
|
description: "Example: v10.0.2"
|
||||||
placeholder: "v10.0.0"
|
placeholder: "v10.0.2"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
34
.github/workflows/main.yml
vendored
34
.github/workflows/main.yml
vendored
@@ -9,12 +9,44 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: python3 make_release.py
|
run: python3 make_release.py
|
||||||
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: plugin
|
name: plugin
|
||||||
path: |
|
path: |
|
||||||
DeDRM_tools_*.zip
|
DeDRM_tools_*.zip
|
||||||
DeDRM_tools.zip
|
DeDRM_tools.zip
|
||||||
|
|
||||||
|
- name: Prepare release
|
||||||
|
run: cp DeDRM_tools.zip DeDRM_alpha_${{ github.sha }}.zip
|
||||||
|
|
||||||
|
|
||||||
|
- uses: dev-drprasad/delete-older-releases@v0.2.1
|
||||||
|
with:
|
||||||
|
repo: noDRM/DeDRM_tools_autorelease
|
||||||
|
keep_latest: 0
|
||||||
|
delete_tags: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.AUTORELEASE_KEY }}
|
||||||
|
|
||||||
|
- name: Auto-release
|
||||||
|
id: autorelease
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: autorelease_${{ github.sha }}
|
||||||
|
repository: noDRM/DeDRM_tools_autorelease
|
||||||
|
token: ${{ secrets.AUTORELEASE_KEY }}
|
||||||
|
name: Automatic alpha release with latest changes
|
||||||
|
body: |
|
||||||
|
This release is automatically generated by Github for each commit.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Last update based on Git commit [${{ github.sha }}](https://github.com/noDRM/DeDRM_tools/commit/${{ github.sha }}).
|
||||||
|
prerelease: true
|
||||||
|
draft: false
|
||||||
|
files: DeDRM_alpha_${{ github.sha }}.zip
|
||||||
|
|||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -35,7 +35,7 @@ List of changes since the fork of Apprentice Harper's repository:
|
|||||||
- Fix small issue with elibri watermark removal.
|
- Fix small issue with elibri watermark removal.
|
||||||
- Adobe key name will now contain account email.
|
- Adobe key name will now contain account email.
|
||||||
|
|
||||||
## Fixes on master (not yet released):
|
## Fixes in v10.0.3 (2022-07-13):
|
||||||
|
|
||||||
- Fix issue where importing a key from Adobe Digital Editions would fail in Python2 (Calibre < 5) if there were non-ASCII characters in the username.
|
- Fix issue where importing a key from Adobe Digital Editions would fail in Python2 (Calibre < 5) if there were non-ASCII characters in the username.
|
||||||
- Add code to support importing multiple decryption keys from ADE.
|
- Add code to support importing multiple decryption keys from ADE.
|
||||||
@@ -56,5 +56,58 @@ List of changes since the fork of Apprentice Harper's repository:
|
|||||||
- Drop support for importing key data from the ancient, pre "DeDRM" Calibre plugins ("Ignoble Epub DeDRM", "eReader PDB 2 PML" and "K4MobiDeDRM"). These are from 2011, I doubt anyone still has these installed, I can't even find a working link for these to test them. If you still have encryption keys in one of these plugins, you will need to update to DeDRM v10.0.2 or older (to convert the keys) before updating to DeDRM v10.0.3 or newer.
|
- Drop support for importing key data from the ancient, pre "DeDRM" Calibre plugins ("Ignoble Epub DeDRM", "eReader PDB 2 PML" and "K4MobiDeDRM"). These are from 2011, I doubt anyone still has these installed, I can't even find a working link for these to test them. If you still have encryption keys in one of these plugins, you will need to update to DeDRM v10.0.2 or older (to convert the keys) before updating to DeDRM v10.0.3 or newer.
|
||||||
- Some Python3 bugfixes for Amazon books (merged #10 by ableeker).
|
- Some Python3 bugfixes for Amazon books (merged #10 by ableeker).
|
||||||
- Fix a bug where extracting an Adobe key from ADE on Linux through Wine did fail when using the OpenSSL backend (instead of PyCrypto). See #13 and #14 for details, thanks acaloiaro for the bugfix.
|
- Fix a bug where extracting an Adobe key from ADE on Linux through Wine did fail when using the OpenSSL backend (instead of PyCrypto). See #13 and #14 for details, thanks acaloiaro for the bugfix.
|
||||||
- Make the plugin work on Calibre 6 (Qt 6). If you're running the Calibre 6 beta and you notice any issues, please open a bug report.
|
|
||||||
- Fix IndexError when DeDRMing some Amazon eBooks.
|
- Fix IndexError when DeDRMing some Amazon eBooks.
|
||||||
|
- Add support for books with the new ADE3.0+ DRM by merging #48 by a980e066a01. Thanks a lot! (Also fixes #96 on MacOS)
|
||||||
|
- Remove OpenSSL support, now the plugin will always use the Python crypto libraries.
|
||||||
|
- Obok: Fix issues with invalid UTF-8 characters by merging #26 by baby-bell.
|
||||||
|
- ineptpdf: Fix broken V=3 key obfuscation algorithm.
|
||||||
|
- ineptpdf: (Hopefully) fix issues with some B&N PDF files.
|
||||||
|
- Fix broken Amazon K4PC key retrieval (fixes #38)
|
||||||
|
- Fix bug that corrupts output file for Print-Replica Amazon books (fixes #30).
|
||||||
|
- Fix Nook Study key retrieval code (partially fixes #50).
|
||||||
|
- Make the plugin work on Calibre 6 (Qt 6). (fixes #54 and #98) If you're running Calibre 6 and you notice any issues, please open a bug report.
|
||||||
|
|
||||||
|
## Fixes in v10.0.9 (RC for v10.1.0, 2023-08-02):
|
||||||
|
|
||||||
|
Note that versions v10.0.4(s), v10.0.5(s) and v10.0.6(s) were released by other people in various forks, so I have decided to make a larger version jump so there are no conflicting version numbers / different builds with the same version number.
|
||||||
|
|
||||||
|
This is v10.0.9, a release candidate for v10.1.0. I don't expect there to be major issues / bugs, but since a lot of code has changed in the last year I wanted to get some "extended testing" before this becomes v10.1.0.
|
||||||
|
|
||||||
|
- Fix a bug introduced with #48 that breaks DeDRM'ing on Calibre 4 (fixes #101).
|
||||||
|
- Fix some more Calibre-6 bugs in the Obok plugin (should fix #114).
|
||||||
|
- Fix a bug where invalid Adobe keys could cause the plugin to stop trying subsequent keys (partially fixes #109).
|
||||||
|
- Fix DRM removal sometimes resetting the ZIP's internal "external_attr" value on Calibre 5 and newer.
|
||||||
|
- Fix tons of PDF decryption issues (hopefully fixes #104 and other PDF-related issues).
|
||||||
|
- Small Python 2 / Calibre 4 bugfix for Obok.
|
||||||
|
- Removing ancient AlfCrypto machine code libraries, moving all encryption / decryption to Python code.
|
||||||
|
- General cleanup and removal of dead code.
|
||||||
|
- Fix a bug where ADE account keys weren't automatically imported from the DeACSM plugin when importing a PDF file.
|
||||||
|
- Re-enable Xrefs in exported PDF files since the file corruption bug is hopefully fixed. Please open bug reports if you encounter new issues with PDF files.
|
||||||
|
- Fix a bug that would sometimes cause corrupted keys to be added when adding them through the config dialog (fixes #145, #134, #119, #116, #115, #109).
|
||||||
|
- Update the README (fixes #136) to indicate that Apprentice Harper's version is no longer being updated.
|
||||||
|
- Fix a bug where PDFs with empty arrays (`<>`) in a PDF object failed to decrypt, fixes #183.
|
||||||
|
- Automatically strip whitespace from entered Amazon Kindle serial numbers, should fix #158.
|
||||||
|
- Obok: Add new setting option "Add new entry" for duplicate books to always add them to the Calibre database as a new book. Fixes #148.
|
||||||
|
- Obok: Fix where changing the Calibre UI language to some languages would cause the "duplicate book" setting to reset.
|
||||||
|
- Fix Python3 bug in stylexml2css.php script, fixes #232.
|
||||||
|
- PDF: Ignore invalid PDF objids unless the script is running in strict mode. Fixes some PDFs, apparently. Fixes #233.
|
||||||
|
- Bugfix: EPUBs with remaining content in the encryption.xml after decryption weren't written correctly.
|
||||||
|
- Support for Adobe's 'aes128-cbc-uncompressed' encryption method (fixes #242).
|
||||||
|
- Two bugfixes for Amazon DeDRM from Satuoni ( https://github.com/noDRM/DeDRM_tools/issues/315#issuecomment-1508305428 ) and andrewc12 ( https://github.com/andrewc12/DeDRM_tools/commit/d9233d61f00d4484235863969919059f4d0b2057 ) that might make the plugin work with newer versions.
|
||||||
|
- Fix font decryption not working with some books (fixes #347), thanks for the patch @bydioeds.
|
||||||
|
- Fix a couple unicode errors for Python2 in Kindle and Nook code.
|
||||||
|
|
||||||
|
## Fixes on master (not yet released):
|
||||||
|
|
||||||
|
- Fix a bug where decrypting a 40-bit RC4 pdf with R=2 didn't work.
|
||||||
|
- Fix a bug where decrypting a 256-bit AES pdf with V=5 didn't work.
|
||||||
|
- Fix bugs in kgenpids.py, alfcrypto.py, mobidedrm.py and kindlekey.py that caused it to fail on Python 2 (#380).
|
||||||
|
- Fix some bugs (Python 2 and Python 3) in erdr2pml.py (untested).
|
||||||
|
- Fix file lock bug in androidkindlekey.py on Windows with Calibre >= 7 (untested).
|
||||||
|
- A bunch of updates to the external FileOpen ineptpdf script, might fix #442 (untested).
|
||||||
|
- Fix exception handling on decrypt in ion.py (#662, thanks @C0rn3j).
|
||||||
|
- Fix SHA1 hash function for erdr2pml.py script (#608, thanks @unwiredben).
|
||||||
|
- Make Kobo DRM removal not fail when there are undownloaded ebooks (#384, thanks @precondition).
|
||||||
|
- Fix Obok import failing in Calibre flatpak due to missing ip command (#586 and #585, thanks @jcotton42).
|
||||||
|
- Don't re-pack EPUB if there's no DRM to remove and no postprocessing done (fixes #555).
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ p {margin-top: 0}
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>DeDRM Plugin <span class="version">(v10.0.2)</span></h1>
|
<h1>DeDRM Plugin <span class="version">(v10.0.9 / v10.1.0 RC1)</span></h1>
|
||||||
|
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@ p {margin-top: 0}
|
|||||||
<h3>Installation</h3>
|
<h3>Installation</h3>
|
||||||
<p>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).</p>
|
<p>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).</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
<h3>Configuration</h3>
|
<h3>Configuration</h3>
|
||||||
<p>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).</p>
|
<p>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).</p>
|
||||||
|
|
||||||
@@ -39,14 +41,14 @@ p {margin-top: 0}
|
|||||||
|
|
||||||
<h3>Troubleshooting:</h3>
|
<h3>Troubleshooting:</h3>
|
||||||
|
|
||||||
<p >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.</p>
|
<p>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.</p>
|
||||||
|
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
<p><span class="bold">Note:</span> 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.</p>
|
<p><span class="bold">Note:</span> 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.</p>
|
||||||
|
|
||||||
<h3>Credits:</h3>
|
<h3>Credits:</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>NoDRM for a bunch of updates and maintenance since November 2021, and the Readium LCP support</li>
|
<li>NoDRM for a bunch of updates and maintenance since November 2021<s>, and the Readium LCP support</s></li>
|
||||||
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
|
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
|
||||||
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
|
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
|
||||||
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
|
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
|
||||||
@@ -60,7 +62,7 @@ p {margin-top: 0}
|
|||||||
<li>And probably many more.</li>
|
<li>And probably many more.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>For additional help read the <a href="https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/noDRM/DeDRM_tools">NoDRM's GitHub repository</a> (or the corresponding <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>). You can <a href="https://github.com/noDRM/DeDRM_tools/issues">open issue reports</a>related to this fork at NoDRM's GitHub repository.</h3>
|
<h4>For additional help read the <a href="https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/noDRM/DeDRM_tools">NoDRM's GitHub repository</a> (or the corresponding <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>). You can <a href="https://github.com/noDRM/DeDRM_tools/issues">open issue reports</a> related to this fork at NoDRM's GitHub repository.</h4>
|
||||||
|
|
||||||
|
|
||||||
<h2>Linux Systems Only</h2>
|
<h2>Linux Systems Only</h2>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ li {margin-top: 0.5em}
|
|||||||
|
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
<h3>Creating New Keys:</h3>
|
<h3>Creating New Keys:</h3>
|
||||||
|
|
||||||
<p>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. </p>
|
<p>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. </p>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ if "calibre" in sys.modules and sys.version_info[0] == 2:
|
|||||||
if os.path.join(config_dir, "plugins", "DeDRM.zip") not in sys.path:
|
if os.path.join(config_dir, "plugins", "DeDRM.zip") not in sys.path:
|
||||||
sys.path.insert(0, os.path.join(config_dir, "plugins", "DeDRM.zip"))
|
sys.path.insert(0, os.path.join(config_dir, "plugins", "DeDRM.zip"))
|
||||||
|
|
||||||
# Explicitly set the package identifier so we are allowed to import stuff ...
|
if "calibre" in sys.modules:
|
||||||
#__package__ = "DeDRM_plugin"
|
# Explicitly set the package identifier so we are allowed to import stuff ...
|
||||||
|
__package__ = "calibre_plugins.dedrm"
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE_END@@
|
#@@CALIBRE_COMPAT_CODE_END@@
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import print_function
|
|||||||
|
|
||||||
# __init__.py for DeDRM_plugin
|
# __init__.py for DeDRM_plugin
|
||||||
# Copyright © 2008-2020 Apprentice Harper et al.
|
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||||
# Copyright © 2021 NoDRM
|
# Copyright © 2021-2023 NoDRM
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
@@ -82,6 +82,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
# 10.0.0 - First forked version by NoDRM. See CHANGELOG.md for details.
|
# 10.0.0 - First forked version by NoDRM. See CHANGELOG.md for details.
|
||||||
# 10.0.1 - Fixes a bug in the watermark code.
|
# 10.0.1 - Fixes a bug in the watermark code.
|
||||||
# 10.0.2 - Fix Kindle for Mac & update Adobe key retrieval
|
# 10.0.2 - Fix Kindle for Mac & update Adobe key retrieval
|
||||||
|
# For changes made in 10.0.3 and above, see the CHANGELOG.md file
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypt DRMed ebooks.
|
Decrypt DRMed ebooks.
|
||||||
@@ -94,7 +95,26 @@ import traceback
|
|||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
import __version
|
try:
|
||||||
|
try:
|
||||||
|
from . import __version
|
||||||
|
except:
|
||||||
|
import __version
|
||||||
|
except:
|
||||||
|
print("#############################")
|
||||||
|
print("Failed to load the DeDRM plugin")
|
||||||
|
print("Did you bundle this from source code yourself? If so, you'll need to run make_release.py instead to generate a valid plugin file.")
|
||||||
|
print("If you have no idea what the above means, please redownload the most recent version of the plugin from the Github Releases page.")
|
||||||
|
print("If you still receive this error with the released version, please open a bug report and attach the following information:")
|
||||||
|
print("#############################")
|
||||||
|
print("Debug information:")
|
||||||
|
print("__version not found, path is:")
|
||||||
|
print(sys.path)
|
||||||
|
print("I'm at:")
|
||||||
|
print(__file__)
|
||||||
|
print("#############################")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class DeDRMError(Exception):
|
class DeDRMError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -117,30 +137,11 @@ try:
|
|||||||
except:
|
except:
|
||||||
config_dir = ""
|
config_dir = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import utilities
|
||||||
|
except:
|
||||||
|
import utilities
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get safely
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
PLUGIN_NAME = __version.PLUGIN_NAME
|
PLUGIN_NAME = __version.PLUGIN_NAME
|
||||||
PLUGIN_VERSION = __version.PLUGIN_VERSION
|
PLUGIN_VERSION = __version.PLUGIN_VERSION
|
||||||
@@ -148,7 +149,7 @@ PLUGIN_VERSION_TUPLE = __version.PLUGIN_VERSION_TUPLE
|
|||||||
|
|
||||||
class DeDRM(FileTypePlugin):
|
class DeDRM(FileTypePlugin):
|
||||||
name = PLUGIN_NAME
|
name = PLUGIN_NAME
|
||||||
description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Readium LCP, Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
|
description = "Removes DRM from Adobe Adept (including Kobo), Barnes & Noble, Amazon Kindle, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
supported_platforms = ['linux', 'osx', 'windows']
|
||||||
author = "Apprentice Alf, Apprentice Harper, NoDRM, The Dark Reverser and i♥cabbages"
|
author = "Apprentice Alf, Apprentice Harper, NoDRM, The Dark Reverser and i♥cabbages"
|
||||||
version = PLUGIN_VERSION_TUPLE
|
version = PLUGIN_VERSION_TUPLE
|
||||||
@@ -166,12 +167,8 @@ class DeDRM(FileTypePlugin):
|
|||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""
|
"""
|
||||||
Dynamic modules can't be imported/loaded from a zipfile.
|
Extracting a couple Python scripts if running on Linux,
|
||||||
So this routine will extract the appropriate
|
just in case we need to run them in Wine.
|
||||||
library for the target OS and copy it to the 'alfcrypto' subdirectory of
|
|
||||||
calibre's configuration directory. That 'alfcrypto' directory is then
|
|
||||||
inserted into the syspath (as the very first entry) in the run function
|
|
||||||
so the CDLL stuff will work in the alfcrypto.py script.
|
|
||||||
|
|
||||||
The extraction only happens once per version of the plugin
|
The extraction only happens once per version of the plugin
|
||||||
Also perform upgrade of preferences once per version
|
Also perform upgrade of preferences once per version
|
||||||
@@ -192,15 +189,12 @@ class DeDRM(FileTypePlugin):
|
|||||||
os.mkdir(self.alfdir)
|
os.mkdir(self.alfdir)
|
||||||
# only continue if we've never run this version of the plugin before
|
# only continue if we've never run this version of the plugin before
|
||||||
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
|
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
|
||||||
if not os.path.exists(self.verdir):
|
if not os.path.exists(self.verdir) and not iswindows and not isosx:
|
||||||
if iswindows:
|
|
||||||
names = ["alfcrypto.dll","alfcrypto64.dll"]
|
names = ["kindlekey.py","adobekey.py","ignoblekeyNookStudy.py","utilities.py","argv_utils.py"]
|
||||||
elif isosx:
|
|
||||||
names = ["libalfcrypto.dylib"]
|
|
||||||
else:
|
|
||||||
names = ["libalfcrypto32.so","libalfcrypto64.so","kindlekey.py","adobekey.py","subasyncio.py"]
|
|
||||||
lib_dict = self.load_resources(names)
|
lib_dict = self.load_resources(names)
|
||||||
print("{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION))
|
print("{0} v{1}: Copying needed Python scripts from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
|
||||||
for entry, data in lib_dict.items():
|
for entry, data in lib_dict.items():
|
||||||
file_path = os.path.join(self.alfdir, entry)
|
file_path = os.path.join(self.alfdir, entry)
|
||||||
@@ -212,7 +206,7 @@ class DeDRM(FileTypePlugin):
|
|||||||
try:
|
try:
|
||||||
open(file_path,'wb').write(data)
|
open(file_path,'wb').write(data)
|
||||||
except:
|
except:
|
||||||
print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION))
|
print("{0} v{1}: Exception when copying needed python scripts".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -222,12 +216,16 @@ class DeDRM(FileTypePlugin):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def postProcessEPUB(self, path_to_ebook):
|
def postProcessEPUB(self, path_to_ebook, path_to_original_ebook = None):
|
||||||
# This is called after the DRM is removed (or if no DRM was present)
|
# This is called after the DRM is removed (or if no DRM was present)
|
||||||
# It does stuff like de-obfuscating fonts (by calling checkFonts)
|
# It does stuff like de-obfuscating fonts (by calling checkFonts)
|
||||||
# or removing watermarks.
|
# or removing watermarks.
|
||||||
|
|
||||||
postProcessStart = time.time()
|
postProcessStart = time.time()
|
||||||
|
postProcessingNeeded = False
|
||||||
|
|
||||||
|
# Save a backup of the EPUB path after DRM removal but before any postprocessing is done.
|
||||||
|
pre_postprocessing_EPUB_path = path_to_ebook
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import prefs
|
import prefs
|
||||||
@@ -254,6 +252,15 @@ class DeDRM(FileTypePlugin):
|
|||||||
postProcessEnd = time.time()
|
postProcessEnd = time.time()
|
||||||
print("{0} v{1}: Post-processing took {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, postProcessEnd-postProcessStart))
|
print("{0} v{1}: Post-processing took {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, postProcessEnd-postProcessStart))
|
||||||
|
|
||||||
|
|
||||||
|
# If the EPUB is DRM-free (path_to_original_ebook will only be set in this case),
|
||||||
|
# and the post-processing hasn't changed anything in the EPUB,
|
||||||
|
# return the raw original file from path_to_original_ebook from before the
|
||||||
|
# zipfix code was executed.
|
||||||
|
if ((path_to_ebook == pre_postprocessing_EPUB_path) and path_to_original_ebook is not None):
|
||||||
|
print("{0} v{1}: Post-processing didn't do anything on DRM-free EPUB, returning original file".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
return path_to_original_ebook
|
||||||
|
|
||||||
return path_to_ebook
|
return path_to_ebook
|
||||||
|
|
||||||
except:
|
except:
|
||||||
@@ -305,9 +312,9 @@ class DeDRM(FileTypePlugin):
|
|||||||
# import the LCP handler
|
# import the LCP handler
|
||||||
import lcpdedrm
|
import lcpdedrm
|
||||||
|
|
||||||
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
if (lcpdedrm.isLCPbook(inf.name)):
|
||||||
try:
|
try:
|
||||||
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
|
retval = lcpdedrm.decryptLCPbook(inf.name, dedrmprefs['lcp_passphrases'], self)
|
||||||
except:
|
except:
|
||||||
print("Looks like that didn't work:")
|
print("Looks like that didn't work:")
|
||||||
raise
|
raise
|
||||||
@@ -366,7 +373,7 @@ class DeDRM(FileTypePlugin):
|
|||||||
from wineutils import WineGetKeys
|
from wineutils import WineGetKeys
|
||||||
|
|
||||||
scriptpath = os.path.join(self.alfdir,"ignoblekeyNookStudy.py")
|
scriptpath = os.path.join(self.alfdir,"ignoblekeyNookStudy.py")
|
||||||
defaultkeys_study = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
|
defaultkeys_study, defaultnames_study = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
|
||||||
|
|
||||||
except:
|
except:
|
||||||
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
@@ -495,10 +502,10 @@ class DeDRM(FileTypePlugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Found matching key
|
# Found matching key
|
||||||
userkey = codecs.decode(userkeyhex, 'hex')
|
|
||||||
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
of = self.temporary_file(".epub")
|
of = self.temporary_file(".epub")
|
||||||
try:
|
try:
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||||
of.close()
|
of.close()
|
||||||
if result == 0:
|
if result == 0:
|
||||||
@@ -515,12 +522,13 @@ class DeDRM(FileTypePlugin):
|
|||||||
|
|
||||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||||
userkey = codecs.decode(userkeyhex, 'hex')
|
|
||||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
of = self.temporary_file(".epub")
|
of = self.temporary_file(".epub")
|
||||||
|
|
||||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
try:
|
try:
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||||
except ineptepub.ADEPTNewVersionError:
|
except ineptepub.ADEPTNewVersionError:
|
||||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
@@ -633,7 +641,7 @@ class DeDRM(FileTypePlugin):
|
|||||||
|
|
||||||
# Not a Barnes & Noble nor an Adobe Adept
|
# Not a Barnes & Noble nor an Adobe Adept
|
||||||
# Probably a DRM-free EPUB, but we should still check for fonts.
|
# Probably a DRM-free EPUB, but we should still check for fonts.
|
||||||
return self.postProcessEPUB(inf.name)
|
return self.postProcessEPUB(inf.name, path_to_ebook)
|
||||||
|
|
||||||
|
|
||||||
def PDFIneptDecrypt(self, path_to_ebook):
|
def PDFIneptDecrypt(self, path_to_ebook):
|
||||||
@@ -657,11 +665,11 @@ class DeDRM(FileTypePlugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Found matching key
|
# Found matching key
|
||||||
userkey = codecs.decode(userkeyhex, 'hex')
|
|
||||||
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
of = self.temporary_file(".pdf")
|
of = self.temporary_file(".pdf")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||||
of.close()
|
of.close()
|
||||||
if result == 0:
|
if result == 0:
|
||||||
@@ -744,10 +752,10 @@ class DeDRM(FileTypePlugin):
|
|||||||
if newkey is not None:
|
if newkey is not None:
|
||||||
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||||
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
|
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
|
||||||
newkeys.append(keyvalue)
|
newkeys.append(newkey)
|
||||||
newnames.append(newname)
|
newnames.append(newname)
|
||||||
except:
|
except:
|
||||||
pass
|
traceback.print_exc()
|
||||||
|
|
||||||
if len(newkeys) > 0:
|
if len(newkeys) > 0:
|
||||||
try:
|
try:
|
||||||
@@ -781,7 +789,7 @@ class DeDRM(FileTypePlugin):
|
|||||||
|
|
||||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
# Unable to decrypt the PDF with any of the existing keys. Is it a B&N PDF?
|
# Unable to decrypt the PDF with any of the existing keys. Is it a B&N PDF?
|
||||||
@@ -925,6 +933,9 @@ class DeDRM(FileTypePlugin):
|
|||||||
# perhaps we need to get a new default Kindle for Mac/PC key
|
# perhaps we need to get a new default Kindle for Mac/PC key
|
||||||
defaultkeys = []
|
defaultkeys = []
|
||||||
print("{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]))
|
print("{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]))
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
print("{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
print("{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -932,32 +943,39 @@ class DeDRM(FileTypePlugin):
|
|||||||
from kindlekey import kindlekeys
|
from kindlekey import kindlekeys
|
||||||
|
|
||||||
defaultkeys = kindlekeys()
|
defaultkeys = kindlekeys()
|
||||||
|
defaultnames = []
|
||||||
else: # linux
|
else: # linux
|
||||||
from wineutils import WineGetKeys
|
from wineutils import WineGetKeys
|
||||||
|
|
||||||
scriptpath = os.path.join(self.alfdir,"kindlekey.py")
|
scriptpath = os.path.join(self.alfdir,"kindlekey.py")
|
||||||
defaultkeys = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix'])
|
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix'])
|
||||||
except:
|
except:
|
||||||
print("{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
print("{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
newkeys = {}
|
newkeys = {}
|
||||||
|
newnames = []
|
||||||
|
|
||||||
for i,keyvalue in enumerate(defaultkeys):
|
for i,keyvalue in enumerate(defaultkeys):
|
||||||
keyname = "default_key_{0:d}".format(i+1)
|
|
||||||
if keyvalue not in dedrmprefs['kindlekeys'].values():
|
if keyvalue not in dedrmprefs['kindlekeys'].values():
|
||||||
newkeys[keyname] = keyvalue
|
newkeys["key_{0:d}".format(i)] = keyvalue
|
||||||
|
|
||||||
if len(newkeys) > 0:
|
if len(newkeys) > 0:
|
||||||
print("{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
|
print("{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
|
||||||
try:
|
try:
|
||||||
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,list(newkeys.items()),[],[],[],self.starttime)
|
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
|
||||||
decoded = True
|
decoded = True
|
||||||
# store the new successful keys in the defaults
|
# store the new successful keys in the defaults
|
||||||
print("{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
|
print("{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
|
||||||
|
i = 1
|
||||||
for keyvalue in newkeys.values():
|
for keyvalue in newkeys.values():
|
||||||
dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue)
|
while "kindle_key_{0:d}_{1:d}".format(int(time.time()), i) in dedrmprefs['kindlekeys']:
|
||||||
|
i = i + 1
|
||||||
|
dedrmprefs.addnamedvaluetoprefs('kindlekeys',"kindle_key_{0:d}_{1:d}".format(int(time.time()), i),keyvalue)
|
||||||
dedrmprefs.writeprefs()
|
dedrmprefs.writeprefs()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
pass
|
pass
|
||||||
if not decoded:
|
if not decoded:
|
||||||
#if you reached here then no luck raise and exception
|
#if you reached here then no luck raise and exception
|
||||||
@@ -1002,8 +1020,8 @@ class DeDRM(FileTypePlugin):
|
|||||||
def run(self, path_to_ebook):
|
def run(self, path_to_ebook):
|
||||||
|
|
||||||
# make sure any unicode output gets converted safely with 'replace'
|
# make sure any unicode output gets converted safely with 'replace'
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=utilities.SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=utilities.SafeUnbuffered(sys.stderr)
|
||||||
|
|
||||||
print("{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
print("{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
self.starttime = time.time()
|
self.starttime = time.time()
|
||||||
|
|||||||
@@ -5,10 +5,21 @@
|
|||||||
# (CLI interface without Calibre)
|
# (CLI interface without Calibre)
|
||||||
# Copyright © 2021 NoDRM
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOTE: This code is not functional (yet). I started working on it a while ago
|
||||||
|
to make a standalone version of the plugins that could work without Calibre,
|
||||||
|
too, but for now there's only a rough code structure and no working code yet.
|
||||||
|
|
||||||
|
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
|
||||||
|
change in the future.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
# For revision history see __init__.py
|
# For revision history see CHANGELOG.md
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Run DeDRM plugin without Calibre.
|
Run DeDRM plugin without Calibre.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
PLUGIN_NAME = "DeDRM"
|
PLUGIN_NAME = "DeDRM"
|
||||||
__version__ = '10.0.2'
|
__version__ = '10.0.9'
|
||||||
|
|
||||||
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||||
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
# I think this file is unused?
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import tkinter
|
|
||||||
import tkinter.constants
|
|
||||||
|
|
||||||
class ActivityBar(tkinter.Frame):
|
|
||||||
|
|
||||||
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
|
|
||||||
bd=2, relief=tkinter.constants.GROOVE, *args, **kw):
|
|
||||||
tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
|
|
||||||
self._master = master
|
|
||||||
self._interval = interval
|
|
||||||
self._maximum = length
|
|
||||||
self._startx = 0
|
|
||||||
self._barwidth = barwidth
|
|
||||||
self._bardiv = length / barwidth
|
|
||||||
if self._bardiv < 10:
|
|
||||||
self._bardiv = 10
|
|
||||||
stopx = self._startx + self._barwidth
|
|
||||||
if stopx > self._maximum:
|
|
||||||
stopx = self._maximum
|
|
||||||
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
|
||||||
# highlightthickness=0, relief='flat', bd=0)
|
|
||||||
self._canv = tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
|
||||||
highlightthickness=0, relief=relief, bd=bd)
|
|
||||||
self._canv.pack(fill='both', expand=1)
|
|
||||||
self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0)
|
|
||||||
|
|
||||||
self._set()
|
|
||||||
self.bind('<Configure>', self._update_coords)
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
def _update_coords(self, event):
|
|
||||||
'''Updates the position of the rectangle inside the canvas when the size of
|
|
||||||
the widget gets changed.'''
|
|
||||||
# looks like we have to call update_idletasks() twice to make sure
|
|
||||||
# to get the results we expect
|
|
||||||
self._canv.update_idletasks()
|
|
||||||
self._maximum = self._canv.winfo_width()
|
|
||||||
self._startx = 0
|
|
||||||
self._barwidth = self._maximum / self._bardiv
|
|
||||||
if self._barwidth < 2:
|
|
||||||
self._barwidth = 2
|
|
||||||
stopx = self._startx + self._barwidth
|
|
||||||
if stopx > self._maximum:
|
|
||||||
stopx = self._maximum
|
|
||||||
self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height())
|
|
||||||
self._canv.update_idletasks()
|
|
||||||
|
|
||||||
def _set(self):
|
|
||||||
if self._startx < 0:
|
|
||||||
self._startx = 0
|
|
||||||
if self._startx > self._maximum:
|
|
||||||
self._startx = self._startx % self._maximum
|
|
||||||
stopx = self._startx + self._barwidth
|
|
||||||
if stopx > self._maximum:
|
|
||||||
stopx = self._maximum
|
|
||||||
self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height())
|
|
||||||
self._canv.update_idletasks()
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self._running = True
|
|
||||||
self.after(self._interval, self._step)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._running = False
|
|
||||||
self._set()
|
|
||||||
|
|
||||||
def _step(self):
|
|
||||||
if self._running:
|
|
||||||
stepsize = self._barwidth / 4
|
|
||||||
if stepsize < 2:
|
|
||||||
stepsize = 2
|
|
||||||
self._startx += stepsize
|
|
||||||
self._set()
|
|
||||||
self.after(self._interval, self._step)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
# I think this file is unused?
|
|
||||||
|
|
||||||
|
|
||||||
import tkinter
|
|
||||||
import tkinter.constants
|
|
||||||
|
|
||||||
# basic scrolled text widget
|
|
||||||
class ScrolledText(tkinter.Text):
|
|
||||||
def __init__(self, master=None, **kw):
|
|
||||||
self.frame = tkinter.Frame(master)
|
|
||||||
self.vbar = tkinter.Scrollbar(self.frame)
|
|
||||||
self.vbar.pack(side=tkinter.constants.RIGHT, fill=tkinter.constants.Y)
|
|
||||||
kw.update({'yscrollcommand': self.vbar.set})
|
|
||||||
tkinter.Text.__init__(self, self.frame, **kw)
|
|
||||||
self.pack(side=tkinter.constants.LEFT, fill=tkinter.constants.BOTH, expand=True)
|
|
||||||
self.vbar['command'] = self.yview
|
|
||||||
# Copy geometry methods of self.frame without overriding Text
|
|
||||||
# methods = hack!
|
|
||||||
text_meths = list(vars(tkinter.Text).keys())
|
|
||||||
methods = list(vars(tkinter.Pack).keys()) + list(vars(tkinter.Grid).keys()) + list(vars(tkinter.Place).keys())
|
|
||||||
methods = set(methods).difference(text_meths)
|
|
||||||
for m in methods:
|
|
||||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
|
||||||
setattr(self, m, getattr(self.frame, m))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.frame)
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# adobekey.pyw, version 7.1
|
# adobekey.pyw, version 7.4
|
||||||
# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al.
|
# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al.
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
@@ -32,41 +32,24 @@
|
|||||||
# 7.1 - Fix "failed to decrypt user key key" error (read username from registry)
|
# 7.1 - Fix "failed to decrypt user key key" error (read username from registry)
|
||||||
# 7.2 - Fix decryption error on Python2 if there's unicode in the username
|
# 7.2 - Fix decryption error on Python2 if there's unicode in the username
|
||||||
# 7.3 - Fix OpenSSL in Wine
|
# 7.3 - Fix OpenSSL in Wine
|
||||||
|
# 7.4 - Remove OpenSSL support to only support PyCryptodome
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '7.3'
|
__version__ = '7.4'
|
||||||
|
|
||||||
import sys, os, struct, getopt
|
import sys, os, struct, getopt
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
|
||||||
|
from .utilities import SafeUnbuffered
|
||||||
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
@@ -74,41 +57,6 @@ except:
|
|||||||
iswindows = sys.platform.startswith('win')
|
iswindows = sys.platform.startswith('win')
|
||||||
isosx = sys.platform.startswith('darwin')
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["adobekey.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
class ADEPTError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -125,91 +73,18 @@ if iswindows:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import _winreg as winreg
|
import _winreg as winreg
|
||||||
|
|
||||||
def get_fake_windows_libcrypto_path():
|
try:
|
||||||
# There seems to be a bug in Wine where a `find_library('libcrypto-1_1')`
|
from Cryptodome.Cipher import AES
|
||||||
# will not return the path to the libcrypto-1_1.dll file.
|
except ImportError:
|
||||||
# So if we're on Windows, and we didn't find the libcrypto the normal way,
|
from Crypto.Cipher import AES
|
||||||
# lets try a hack-y workaround. It's already over anyways at this
|
|
||||||
# point, can't really make it worse.
|
|
||||||
import sys, os
|
|
||||||
for p in sys.path:
|
|
||||||
if os.path.isfile(os.path.join(p, "libcrypto-1_1.dll")):
|
|
||||||
return os.path.join(p, "libcrypto-1_1.dll")
|
|
||||||
if os.path.isfile(os.path.join(p, "libeay32.dll")):
|
|
||||||
return os.path.join(p, "libeay32.dll")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
def unpad(data, padding=16):
|
||||||
from ctypes.util import find_library
|
if sys.version_info[0] == 2:
|
||||||
libcrypto = find_library('libcrypto-1_1')
|
pad_len = ord(data[-1])
|
||||||
if libcrypto is None:
|
else:
|
||||||
libcrypto = find_library('libeay32')
|
pad_len = data[-1]
|
||||||
if libcrypto is None:
|
|
||||||
libcrypto = get_fake_windows_libcrypto_path()
|
|
||||||
if libcrypto is None:
|
|
||||||
raise ADEPTError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
AES_MAXNR = 14
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
|
||||||
('rounds', c_int)]
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise ADEPTError('AES improper key used')
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise ADEPTError('Failed to initialize AES key')
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
iv = (b"\x00" * self._blocksize)
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise ADEPTError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
try:
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
except (ImportError, ModuleNotFoundError):
|
|
||||||
from Cryptodome.Cipher import AES as _AES
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = None
|
|
||||||
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
|
||||||
try:
|
|
||||||
AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, ModuleNotFoundError, ADEPTError):
|
|
||||||
pass
|
|
||||||
return AES
|
|
||||||
|
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
||||||
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
|
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
|
||||||
@@ -402,8 +277,6 @@ if iswindows:
|
|||||||
CryptUnprotectData = CryptUnprotectData()
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
def adeptkeys():
|
def adeptkeys():
|
||||||
if AES is None:
|
|
||||||
raise ADEPTError("PyCrypto or OpenSSL must be installed")
|
|
||||||
root = GetSystemDirectory().split('\\')[0] + '\\'
|
root = GetSystemDirectory().split('\\')[0] + '\\'
|
||||||
serial = GetVolumeSerialNumber(root)
|
serial = GetVolumeSerialNumber(root)
|
||||||
vendor = cpuid0()
|
vendor = cpuid0()
|
||||||
@@ -416,7 +289,7 @@ if iswindows:
|
|||||||
try:
|
try:
|
||||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||||
except WindowsError, FileNotFoundError:
|
except (WindowsError, FileNotFoundError):
|
||||||
raise ADEPTError("Adobe Digital Editions not activated")
|
raise ADEPTError("Adobe Digital Editions not activated")
|
||||||
keykey = CryptUnprotectData(device, entropy)
|
keykey = CryptUnprotectData(device, entropy)
|
||||||
userkey = None
|
userkey = None
|
||||||
@@ -424,7 +297,7 @@ if iswindows:
|
|||||||
names = []
|
names = []
|
||||||
try:
|
try:
|
||||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||||
except WindowsError, FileNotFoundError:
|
except (WindowsError, FileNotFoundError):
|
||||||
raise ADEPTError("Could not locate ADE activation")
|
raise ADEPTError("Could not locate ADE activation")
|
||||||
|
|
||||||
i = -1
|
i = -1
|
||||||
@@ -443,7 +316,7 @@ if iswindows:
|
|||||||
for j in range(0, 16):
|
for j in range(0, 16):
|
||||||
try:
|
try:
|
||||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||||
except WindowsError, FileNotFoundError:
|
except (WindowsError, FileNotFoundError):
|
||||||
break
|
break
|
||||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||||
if ktype == 'user':
|
if ktype == 'user':
|
||||||
@@ -461,10 +334,7 @@ if iswindows:
|
|||||||
pass
|
pass
|
||||||
if ktype == 'privateLicenseKey':
|
if ktype == 'privateLicenseKey':
|
||||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||||
userkey = b64decode(userkey)
|
userkey = unpad(AES.new(keykey, AES.MODE_CBC, b'\x00'*16).decrypt(b64decode(userkey)))[26:]
|
||||||
aes = AES(keykey)
|
|
||||||
userkey = aes.decrypt(userkey)
|
|
||||||
userkey = userkey[26:-ord(userkey[-1:])]
|
|
||||||
# print ("found " + uuid_name + " key: " + str(userkey))
|
# print ("found " + uuid_name + " key: " + str(userkey))
|
||||||
keys.append(userkey)
|
keys.append(userkey)
|
||||||
|
|
||||||
@@ -584,7 +454,7 @@ def usage(progname):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("adobekey.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
print("{0} v{1}\nCopyright © 2009-2020 i♥cabbages, Apprentice Harper et al.".format(progname,__version__))
|
print("{0} v{1}\nCopyright © 2009-2020 i♥cabbages, Apprentice Harper et al.".format(progname,__version__))
|
||||||
|
|
||||||
@@ -662,7 +532,7 @@ def gui_main():
|
|||||||
self.text.insert(tkinter.constants.END, text)
|
self.text.insert(tkinter.constants.END, text)
|
||||||
|
|
||||||
|
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("adobekey.py")
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
progpath, progname = os.path.split(argv[0])
|
progpath, progname = os.path.split(argv[0])
|
||||||
|
|||||||
@@ -23,13 +23,11 @@ import sys, os, time
|
|||||||
import base64, hashlib
|
import base64, hashlib
|
||||||
try:
|
try:
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
except:
|
except ImportError:
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
|
||||||
|
|
||||||
def unpad(data):
|
|
||||||
|
|
||||||
|
def unpad(data, padding=16):
|
||||||
if sys.version_info[0] == 2:
|
if sys.version_info[0] == 2:
|
||||||
pad_len = ord(data[-1])
|
pad_len = ord(data[-1])
|
||||||
else:
|
else:
|
||||||
@@ -37,6 +35,8 @@ def unpad(data):
|
|||||||
|
|
||||||
return data[:-pad_len]
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
|
|||||||
Binary file not shown.
@@ -8,260 +8,104 @@
|
|||||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||||
|
|
||||||
import sys, os
|
import sys
|
||||||
import hmac
|
import hmac
|
||||||
from struct import pack
|
from struct import pack
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import aescbc
|
||||||
|
|
||||||
# interface to needed routines libalfcrypto
|
class Pukall_Cipher(object):
|
||||||
def _load_libalfcrypto():
|
def __init__(self):
|
||||||
import ctypes
|
self.key = None
|
||||||
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof
|
|
||||||
|
|
||||||
pointer_size = ctypes.sizeof(ctypes.c_voidp)
|
def PC1(self, key, src, decryption=True):
|
||||||
name_of_lib = None
|
sum1 = 0;
|
||||||
if sys.platform.startswith('darwin'):
|
sum2 = 0;
|
||||||
name_of_lib = 'libalfcrypto.dylib'
|
keyXorVal = 0;
|
||||||
elif sys.platform.startswith('win'):
|
if len(key)!=16:
|
||||||
if pointer_size == 4:
|
raise Exception("PC1: Bad key length")
|
||||||
name_of_lib = 'alfcrypto.dll'
|
wkey = []
|
||||||
else:
|
for i in range(8):
|
||||||
name_of_lib = 'alfcrypto64.dll'
|
if sys.version_info[0] == 2:
|
||||||
else:
|
|
||||||
if pointer_size == 4:
|
|
||||||
name_of_lib = 'libalfcrypto32.so'
|
|
||||||
else:
|
|
||||||
name_of_lib = 'libalfcrypto64.so'
|
|
||||||
|
|
||||||
# hard code to local location for libalfcrypto
|
|
||||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
|
||||||
if not os.path.isfile(libalfcrypto):
|
|
||||||
libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib)
|
|
||||||
if not os.path.isfile(libalfcrypto):
|
|
||||||
libalfcrypto = os.path.join('.',name_of_lib)
|
|
||||||
if not os.path.isfile(libalfcrypto):
|
|
||||||
raise Exception('libalfcrypto not found at %s' % libalfcrypto)
|
|
||||||
|
|
||||||
libalfcrypto = CDLL(libalfcrypto)
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libalfcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
# aes cbc decryption
|
|
||||||
#
|
|
||||||
# struct aes_key_st {
|
|
||||||
# unsigned long rd_key[4 *(AES_MAXNR + 1)];
|
|
||||||
# int rounds;
|
|
||||||
# };
|
|
||||||
#
|
|
||||||
# typedef struct aes_key_st AES_KEY;
|
|
||||||
#
|
|
||||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
|
||||||
# const unsigned long length, const AES_KEY *key,
|
|
||||||
# unsigned char *ivec, const int enc);
|
|
||||||
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
|
||||||
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int])
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Pukall 1 Cipher
|
|
||||||
# unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src,
|
|
||||||
# unsigned char *dest, unsigned int len, int decryption);
|
|
||||||
|
|
||||||
PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong])
|
|
||||||
|
|
||||||
# Topaz Encryption
|
|
||||||
# typedef struct _TpzCtx {
|
|
||||||
# unsigned int v[2];
|
|
||||||
# } TpzCtx;
|
|
||||||
#
|
|
||||||
# void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen);
|
|
||||||
# void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len);
|
|
||||||
|
|
||||||
class TPZ_CTX(Structure):
|
|
||||||
_fields_ = [('v', c_long * 2)]
|
|
||||||
|
|
||||||
TPZ_CTX_p = POINTER(TPZ_CTX)
|
|
||||||
topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong])
|
|
||||||
topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong])
|
|
||||||
|
|
||||||
|
|
||||||
class AES_CBC(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._blocksize = 0
|
|
||||||
self._keyctx = None
|
|
||||||
self._iv = 0
|
|
||||||
|
|
||||||
def set_decrypt_key(self, userkey, iv):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise Exception('AES CBC improper key used')
|
|
||||||
return
|
|
||||||
keyctx = self._keyctx = AES_KEY()
|
|
||||||
self._iv = iv
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
|
||||||
if rv < 0:
|
|
||||||
raise Exception('Failed to initialize AES CBC key')
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
mutable_iv = create_string_buffer(self._iv, len(self._iv))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise Exception('AES CBC decryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
class Pukall_Cipher(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.key = None
|
|
||||||
|
|
||||||
def PC1(self, key, src, decryption=True):
|
|
||||||
self.key = key
|
|
||||||
out = create_string_buffer(len(src))
|
|
||||||
de = 0
|
|
||||||
if decryption:
|
|
||||||
de = 1
|
|
||||||
rv = PC1(key, len(key), src, out, len(src), de)
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
class Topaz_Cipher(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._ctx = None
|
|
||||||
|
|
||||||
def ctx_init(self, key):
|
|
||||||
tpz_ctx = self._ctx = TPZ_CTX()
|
|
||||||
topazCryptoInit(tpz_ctx, key, len(key))
|
|
||||||
return tpz_ctx
|
|
||||||
|
|
||||||
def decrypt(self, data, ctx=None):
|
|
||||||
if ctx == None:
|
|
||||||
ctx = self._ctx
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
topazCryptoDecrypt(ctx, data, out, len(data))
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
print("Using Library AlfCrypto DLL/DYLIB/SO")
|
|
||||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_python_alfcrypto():
|
|
||||||
|
|
||||||
import aescbc
|
|
||||||
|
|
||||||
class Pukall_Cipher(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.key = None
|
|
||||||
|
|
||||||
def PC1(self, key, src, decryption=True):
|
|
||||||
sum1 = 0;
|
|
||||||
sum2 = 0;
|
|
||||||
keyXorVal = 0;
|
|
||||||
if len(key)!=16:
|
|
||||||
raise Exception('Pukall_Cipher: Bad key length.')
|
|
||||||
wkey = []
|
|
||||||
for i in range(8):
|
|
||||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||||
dst = ""
|
else:
|
||||||
for i in range(len(src)):
|
wkey.append(key[i*2]<<8 | key[i*2+1])
|
||||||
temp1 = 0;
|
dst = bytearray(len(src))
|
||||||
byteXorVal = 0;
|
for i in range(len(src)):
|
||||||
for j in range(8):
|
temp1 = 0;
|
||||||
temp1 ^= wkey[j]
|
byteXorVal = 0;
|
||||||
sum2 = (sum2+j)*20021 + sum1
|
for j in range(8):
|
||||||
sum1 = (temp1*346)&0xFFFF
|
temp1 ^= wkey[j]
|
||||||
sum2 = (sum2+sum1)&0xFFFF
|
sum2 = (sum2+j)*20021 + sum1
|
||||||
temp1 = (temp1*20021+1)&0xFFFF
|
sum1 = (temp1*346)&0xFFFF
|
||||||
byteXorVal ^= temp1 ^ sum2
|
sum2 = (sum2+sum1)&0xFFFF
|
||||||
|
temp1 = (temp1*20021+1)&0xFFFF
|
||||||
|
byteXorVal ^= temp1 ^ sum2
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
curByte = ord(src[i])
|
curByte = ord(src[i])
|
||||||
if not decryption:
|
else:
|
||||||
keyXorVal = curByte * 257;
|
curByte = src[i]
|
||||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
|
||||||
if decryption:
|
|
||||||
keyXorVal = curByte * 257;
|
|
||||||
for j in range(8):
|
|
||||||
wkey[j] ^= keyXorVal;
|
|
||||||
dst+=chr(curByte)
|
|
||||||
return dst
|
|
||||||
|
|
||||||
class Topaz_Cipher(object):
|
if not decryption:
|
||||||
def __init__(self):
|
keyXorVal = curByte * 257;
|
||||||
self._ctx = None
|
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||||
|
if decryption:
|
||||||
|
keyXorVal = curByte * 257;
|
||||||
|
for j in range(8):
|
||||||
|
wkey[j] ^= keyXorVal;
|
||||||
|
|
||||||
def ctx_init(self, key):
|
if sys.version_info[0] == 2:
|
||||||
ctx1 = 0x0CAFFE19E
|
dst[i] = chr(curByte)
|
||||||
if isinstance(key, str):
|
else:
|
||||||
key = key.encode('latin-1')
|
dst[i] = curByte
|
||||||
for keyByte in key:
|
|
||||||
ctx2 = ctx1
|
|
||||||
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
|
||||||
self._ctx = [ctx1, ctx2]
|
|
||||||
return [ctx1,ctx2]
|
|
||||||
|
|
||||||
def decrypt(self, data, ctx=None):
|
return bytes(dst)
|
||||||
if ctx == None:
|
|
||||||
ctx = self._ctx
|
|
||||||
ctx1 = ctx[0]
|
|
||||||
ctx2 = ctx[1]
|
|
||||||
plainText = ""
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode('latin-1')
|
|
||||||
for dataByte in data:
|
|
||||||
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
|
||||||
ctx2 = ctx1
|
|
||||||
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
|
||||||
plainText += chr(m)
|
|
||||||
return plainText
|
|
||||||
|
|
||||||
class AES_CBC(object):
|
class Topaz_Cipher(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._key = None
|
self._ctx = None
|
||||||
self._iv = None
|
|
||||||
self.aes = None
|
|
||||||
|
|
||||||
def set_decrypt_key(self, userkey, iv):
|
def ctx_init(self, key):
|
||||||
self._key = userkey
|
ctx1 = 0x0CAFFE19E
|
||||||
self._iv = iv
|
if isinstance(key, str):
|
||||||
self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
|
key = key.encode('latin-1')
|
||||||
|
for keyByte in key:
|
||||||
|
ctx2 = ctx1
|
||||||
|
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||||
|
self._ctx = [ctx1, ctx2]
|
||||||
|
return [ctx1,ctx2]
|
||||||
|
|
||||||
def decrypt(self, data):
|
def decrypt(self, data, ctx=None):
|
||||||
iv = self._iv
|
if ctx == None:
|
||||||
cleartext = self.aes.decrypt(iv + data)
|
ctx = self._ctx
|
||||||
return cleartext
|
ctx1 = ctx[0]
|
||||||
|
ctx2 = ctx[1]
|
||||||
|
plainText = ""
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('latin-1')
|
||||||
|
for dataByte in data:
|
||||||
|
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||||
|
ctx2 = ctx1
|
||||||
|
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||||
|
plainText += chr(m)
|
||||||
|
return plainText
|
||||||
|
|
||||||
print("Using Library AlfCrypto Python")
|
class AES_CBC(object):
|
||||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
def __init__(self):
|
||||||
|
self._key = None
|
||||||
|
self._iv = None
|
||||||
|
self.aes = None
|
||||||
|
|
||||||
|
def set_decrypt_key(self, userkey, iv):
|
||||||
|
self._key = userkey
|
||||||
|
self._iv = iv
|
||||||
|
self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
|
||||||
|
|
||||||
def _load_crypto():
|
def decrypt(self, data):
|
||||||
AES_CBC = Pukall_Cipher = Topaz_Cipher = None
|
iv = self._iv
|
||||||
cryptolist = (_load_libalfcrypto, _load_python_alfcrypto)
|
cleartext = self.aes.decrypt(iv + data)
|
||||||
for loader in cryptolist:
|
return cleartext
|
||||||
try:
|
|
||||||
AES_CBC, Pukall_Cipher, Topaz_Cipher = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, Exception):
|
|
||||||
pass
|
|
||||||
return AES_CBC, Pukall_Cipher, Topaz_Cipher
|
|
||||||
|
|
||||||
AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto()
|
|
||||||
|
|
||||||
|
|
||||||
class KeyIVGen(object):
|
class KeyIVGen(object):
|
||||||
@@ -273,7 +117,7 @@ class KeyIVGen(object):
|
|||||||
def xorbytes( a, b ):
|
def xorbytes( a, b ):
|
||||||
if len(a) != len(b):
|
if len(a) != len(b):
|
||||||
raise Exception("xorbytes(): lengths differ")
|
raise Exception("xorbytes(): lengths differ")
|
||||||
return bytes([x ^ y for x, y in zip(a, b)])
|
return bytes(bytearray([x ^ y for x, y in zip(a, b)]))
|
||||||
|
|
||||||
def prf( h, data ):
|
def prf( h, data ):
|
||||||
hm = h.copy()
|
hm = h.copy()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# androidkindlekey.py
|
# androidkindlekey.py
|
||||||
# Copyright © 2010-20 by Thom, Apprentice Harper et al.
|
# Copyright © 2010-22 by Thom, Apprentice Harper et al.
|
||||||
|
|
||||||
# Revision history:
|
# Revision history:
|
||||||
# 1.0 - AmazonSecureStorage.xml decryption to serial number
|
# 1.0 - AmazonSecureStorage.xml decryption to serial number
|
||||||
@@ -14,13 +14,14 @@
|
|||||||
# 1.4 - Fix some problems identified by Aldo Bleeker
|
# 1.4 - Fix some problems identified by Aldo Bleeker
|
||||||
# 1.5 - Fix another problem identified by Aldo Bleeker
|
# 1.5 - Fix another problem identified by Aldo Bleeker
|
||||||
# 2.0 - Python 3 compatibility
|
# 2.0 - Python 3 compatibility
|
||||||
|
# 2.1 - Remove OpenSSL support; only support PyCryptodome
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Kindle for Android Serial Number.
|
Retrieve Kindle for Android Serial Number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '2.0'
|
__version__ = '2.1'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -33,73 +34,13 @@ from hashlib import md5
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from binascii import a2b_hex, b2a_hex
|
from binascii import a2b_hex, b2a_hex
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.Cipher import AES, DES
|
||||||
|
except ImportError:
|
||||||
|
from Crypto.Cipher import AES, DES
|
||||||
|
|
||||||
# Routines common to Mac and PC
|
# Routines common to Mac and PC
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
except:
|
|
||||||
iswindows = sys.platform.startswith('win')
|
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["kindlekey.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -108,6 +49,20 @@ STORAGE = "backup.ab"
|
|||||||
STORAGE1 = "AmazonSecureStorage.xml"
|
STORAGE1 = "AmazonSecureStorage.xml"
|
||||||
STORAGE2 = "map_data_storage.db"
|
STORAGE2 = "map_data_storage.db"
|
||||||
|
|
||||||
|
|
||||||
|
def unpad(data, padding=16):
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
pad_len = ord(data[-1])
|
||||||
|
else:
|
||||||
|
pad_len = data[-1]
|
||||||
|
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
def pad(data, padding_len=16):
|
||||||
|
padding_data_len = padding_len - (len(data) % padding_len)
|
||||||
|
plaintext = data + chr(padding_data_len) * padding_data_len
|
||||||
|
return plaintext
|
||||||
|
|
||||||
class AndroidObfuscation(object):
|
class AndroidObfuscation(object):
|
||||||
'''AndroidObfuscation
|
'''AndroidObfuscation
|
||||||
For the key, it's written in java, and run in android dalvikvm
|
For the key, it's written in java, and run in android dalvikvm
|
||||||
@@ -115,24 +70,16 @@ class AndroidObfuscation(object):
|
|||||||
|
|
||||||
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
|
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
|
||||||
|
|
||||||
|
def _get_cipher(self):
|
||||||
|
return AES.new(self.key, AES.MODE_ECB)
|
||||||
|
|
||||||
def encrypt(self, plaintext):
|
def encrypt(self, plaintext):
|
||||||
cipher = self._get_cipher()
|
pt = pad(plaintext.encode('utf-8'), 16)
|
||||||
padding = len(self.key) - len(plaintext) % len(self.key)
|
return b2a_hex(self._get_cipher().encrypt(pt))
|
||||||
plaintext += chr(padding) * padding
|
|
||||||
return b2a_hex(cipher.encrypt(plaintext.encode('utf-8')))
|
|
||||||
|
|
||||||
def decrypt(self, ciphertext):
|
def decrypt(self, ciphertext):
|
||||||
cipher = self._get_cipher()
|
ct = a2b_hex(ciphertext)
|
||||||
plaintext = cipher.decrypt(a2b_hex(ciphertext))
|
return unpad(self._get_cipher().decrypt(ct), 16)
|
||||||
return plaintext[:-ord(plaintext[-1])]
|
|
||||||
|
|
||||||
def _get_cipher(self):
|
|
||||||
try:
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
return AES.new(self.key)
|
|
||||||
except ImportError:
|
|
||||||
from aescbc import AES, noPadding
|
|
||||||
return AES(self.key, padding=noPadding())
|
|
||||||
|
|
||||||
class AndroidObfuscationV2(AndroidObfuscation):
|
class AndroidObfuscationV2(AndroidObfuscation):
|
||||||
'''AndroidObfuscationV2
|
'''AndroidObfuscationV2
|
||||||
@@ -149,12 +96,7 @@ class AndroidObfuscationV2(AndroidObfuscation):
|
|||||||
self.iv = key[8:16]
|
self.iv = key[8:16]
|
||||||
|
|
||||||
def _get_cipher(self):
|
def _get_cipher(self):
|
||||||
try :
|
return DES.new(self.key, DES.MODE_CBC, self.iv)
|
||||||
from Crypto.Cipher import DES
|
|
||||||
return DES.new(self.key, DES.MODE_CBC, self.iv)
|
|
||||||
except ImportError:
|
|
||||||
from python_des import Des, CBC
|
|
||||||
return Des(self.key, CBC, self.iv)
|
|
||||||
|
|
||||||
def parse_preference(path):
|
def parse_preference(path):
|
||||||
''' parse android's shared preference xml '''
|
''' parse android's shared preference xml '''
|
||||||
@@ -259,6 +201,9 @@ def get_serials2(path=STORAGE2):
|
|||||||
for y in tokens:
|
for y in tokens:
|
||||||
serials.append(y)
|
serials.append(y)
|
||||||
serials.append(x+y)
|
serials.append(x+y)
|
||||||
|
|
||||||
|
connection.close()
|
||||||
|
|
||||||
return serials
|
return serials
|
||||||
|
|
||||||
def get_serials(path=STORAGE):
|
def get_serials(path=STORAGE):
|
||||||
@@ -335,9 +280,7 @@ def usage(progname):
|
|||||||
|
|
||||||
|
|
||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
argv=sys.argv
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
|
||||||
argv=unicode_argv()
|
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))
|
print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))
|
||||||
|
|
||||||
@@ -456,7 +399,7 @@ def gui_main():
|
|||||||
return
|
return
|
||||||
self.status['text'] = "Select backup.ab file"
|
self.status['text'] = "Select backup.ab file"
|
||||||
|
|
||||||
argv=unicode_argv()
|
argv=sys.argv()
|
||||||
progpath, progname = os.path.split(argv[0])
|
progpath, progname = os.path.split(argv[0])
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
|
root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import sys, os
|
import sys
|
||||||
import locale
|
|
||||||
import codecs
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
# get sys.argv arguments and encode them into utf-8
|
# get sys.argv arguments and encode them into utf-8
|
||||||
def unicode_argv():
|
def unicode_argv(default_name):
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
|
try:
|
||||||
|
from calibre.constants import iswindows
|
||||||
|
except:
|
||||||
|
iswindows = sys.platform.startswith('win')
|
||||||
|
|
||||||
|
if iswindows:
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||||
# strings.
|
# strings.
|
||||||
|
|
||||||
@@ -38,50 +41,8 @@ def unicode_argv():
|
|||||||
range(start, argc.value)]
|
range(start, argc.value)]
|
||||||
# if we don't have any arguments at all, just pass back script name
|
# if we don't have any arguments at all, just pass back script name
|
||||||
# this should never happen
|
# this should never happen
|
||||||
return ["DeDRM.py"]
|
return [ default_name ]
|
||||||
else:
|
else:
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
argvencoding = sys.stdin.encoding or "utf-8"
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||||
|
|
||||||
|
|
||||||
def add_cp65001_codec():
|
|
||||||
try:
|
|
||||||
codecs.lookup('cp65001')
|
|
||||||
except LookupError:
|
|
||||||
codecs.register(
|
|
||||||
lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def set_utf8_default_encoding():
|
|
||||||
if sys.getdefaultencoding() == 'utf-8':
|
|
||||||
return
|
|
||||||
|
|
||||||
# Regenerate setdefaultencoding.
|
|
||||||
importlib.reload(sys)
|
|
||||||
sys.setdefaultencoding('utf-8')
|
|
||||||
|
|
||||||
for attr in dir(locale):
|
|
||||||
if attr[0:3] != 'LC_':
|
|
||||||
continue
|
|
||||||
aref = getattr(locale, attr)
|
|
||||||
try:
|
|
||||||
locale.setlocale(aref, '')
|
|
||||||
except locale.Error:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
lang = locale.getlocale(aref)[0]
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if lang:
|
|
||||||
try:
|
|
||||||
locale.setlocale(aref, (lang, 'UTF-8'))
|
|
||||||
except locale.Error:
|
|
||||||
os.environ[attr] = lang + '.UTF-8'
|
|
||||||
try:
|
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
|
||||||
except locale.Error:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
# to work around tk_chooseDirectory not properly returning unicode paths on Windows
|
|
||||||
# need to use a dialog that can be hacked up to actually return full unicode paths
|
|
||||||
# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
|
|
||||||
# to actually use unicode for path
|
|
||||||
|
|
||||||
# The original license for EasyDialogs is as follows
|
|
||||||
#
|
|
||||||
# Copyright (c) 2003-2005 Jimmy Retzlaff
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
||||||
# copy of this software and associated documentation files (the "Software"),
|
|
||||||
# to deal in the Software without restriction, including without limitation
|
|
||||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
||||||
# and/or sell copies of the Software, and to permit persons to whom the
|
|
||||||
# Software is furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in
|
|
||||||
# all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
# DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
# Adjusted for Python 3, September 2020
|
|
||||||
|
|
||||||
"""
|
|
||||||
AskFolder(...) -- Ask the user to select a folder Windows specific
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
import ctypes.wintypes as wintypes
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['AskFolder']
|
|
||||||
|
|
||||||
# Load required Windows DLLs
|
|
||||||
ole32 = ctypes.windll.ole32
|
|
||||||
shell32 = ctypes.windll.shell32
|
|
||||||
user32 = ctypes.windll.user32
|
|
||||||
|
|
||||||
|
|
||||||
# Windows Constants
|
|
||||||
BFFM_INITIALIZED = 1
|
|
||||||
BFFM_SETOKTEXT = 1129
|
|
||||||
BFFM_SETSELECTIONA = 1126
|
|
||||||
BFFM_SETSELECTIONW = 1127
|
|
||||||
BIF_EDITBOX = 16
|
|
||||||
BS_DEFPUSHBUTTON = 1
|
|
||||||
CB_ADDSTRING = 323
|
|
||||||
CB_GETCURSEL = 327
|
|
||||||
CB_SETCURSEL = 334
|
|
||||||
CDM_SETCONTROLTEXT = 1128
|
|
||||||
EM_GETLINECOUNT = 186
|
|
||||||
EM_GETMARGINS = 212
|
|
||||||
EM_POSFROMCHAR = 214
|
|
||||||
EM_SETSEL = 177
|
|
||||||
GWL_STYLE = -16
|
|
||||||
IDC_STATIC = -1
|
|
||||||
IDCANCEL = 2
|
|
||||||
IDNO = 7
|
|
||||||
IDOK = 1
|
|
||||||
IDYES = 6
|
|
||||||
MAX_PATH = 260
|
|
||||||
OFN_ALLOWMULTISELECT = 512
|
|
||||||
OFN_ENABLEHOOK = 32
|
|
||||||
OFN_ENABLESIZING = 8388608
|
|
||||||
OFN_ENABLETEMPLATEHANDLE = 128
|
|
||||||
OFN_EXPLORER = 524288
|
|
||||||
OFN_OVERWRITEPROMPT = 2
|
|
||||||
OPENFILENAME_SIZE_VERSION_400 = 76
|
|
||||||
PBM_GETPOS = 1032
|
|
||||||
PBM_SETMARQUEE = 1034
|
|
||||||
PBM_SETPOS = 1026
|
|
||||||
PBM_SETRANGE = 1025
|
|
||||||
PBM_SETRANGE32 = 1030
|
|
||||||
PBS_MARQUEE = 8
|
|
||||||
PM_REMOVE = 1
|
|
||||||
SW_HIDE = 0
|
|
||||||
SW_SHOW = 5
|
|
||||||
SW_SHOWNORMAL = 1
|
|
||||||
SWP_NOACTIVATE = 16
|
|
||||||
SWP_NOMOVE = 2
|
|
||||||
SWP_NOSIZE = 1
|
|
||||||
SWP_NOZORDER = 4
|
|
||||||
VER_PLATFORM_WIN32_NT = 2
|
|
||||||
WM_COMMAND = 273
|
|
||||||
WM_GETTEXT = 13
|
|
||||||
WM_GETTEXTLENGTH = 14
|
|
||||||
WM_INITDIALOG = 272
|
|
||||||
WM_NOTIFY = 78
|
|
||||||
|
|
||||||
# Windows function prototypes
|
|
||||||
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
|
|
||||||
|
|
||||||
# Windows types
|
|
||||||
LPCTSTR = ctypes.c_char_p
|
|
||||||
LPTSTR = ctypes.c_char_p
|
|
||||||
LPVOID = ctypes.c_voidp
|
|
||||||
TCHAR = ctypes.c_char
|
|
||||||
|
|
||||||
class BROWSEINFO(ctypes.Structure):
|
|
||||||
_fields_ = [
|
|
||||||
("hwndOwner", wintypes.HWND),
|
|
||||||
("pidlRoot", LPVOID),
|
|
||||||
("pszDisplayName", LPTSTR),
|
|
||||||
("lpszTitle", LPCTSTR),
|
|
||||||
("ulFlags", ctypes.c_uint),
|
|
||||||
("lpfn", BrowseCallbackProc),
|
|
||||||
("lParam", wintypes.LPARAM),
|
|
||||||
("iImage", ctypes.c_int)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
def CenterWindow(hwnd):
|
|
||||||
desktopRect = GetWindowRect(user32.GetDesktopWindow())
|
|
||||||
myRect = GetWindowRect(hwnd)
|
|
||||||
x = width(desktopRect) // 2 - width(myRect) // 2
|
|
||||||
y = height(desktopRect) // 2 - height(myRect) // 2
|
|
||||||
user32.SetWindowPos(hwnd, 0,
|
|
||||||
desktopRect.left + x,
|
|
||||||
desktopRect.top + y,
|
|
||||||
0, 0,
|
|
||||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def GetWindowRect(hwnd):
|
|
||||||
rect = wintypes.RECT()
|
|
||||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
|
||||||
return rect
|
|
||||||
|
|
||||||
def width(rect):
|
|
||||||
return rect.right-rect.left
|
|
||||||
|
|
||||||
def height(rect):
|
|
||||||
return rect.bottom-rect.top
|
|
||||||
|
|
||||||
|
|
||||||
def AskFolder(
|
|
||||||
message=None,
|
|
||||||
version=None,
|
|
||||||
defaultLocation=None,
|
|
||||||
location=None,
|
|
||||||
windowTitle=None,
|
|
||||||
actionButtonLabel=None,
|
|
||||||
cancelButtonLabel=None,
|
|
||||||
multiple=None):
|
|
||||||
"""Display a dialog asking the user for select a folder.
|
|
||||||
modified to use unicode strings as much as possible
|
|
||||||
returns unicode path
|
|
||||||
"""
|
|
||||||
|
|
||||||
def BrowseCallback(hwnd, uMsg, lParam, lpData):
|
|
||||||
if uMsg == BFFM_INITIALIZED:
|
|
||||||
if actionButtonLabel:
|
|
||||||
label = str(actionButtonLabel, errors='replace')
|
|
||||||
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
|
|
||||||
if cancelButtonLabel:
|
|
||||||
label = str(cancelButtonLabel, errors='replace')
|
|
||||||
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
|
|
||||||
if cancelButton:
|
|
||||||
user32.SetWindowTextW(cancelButton, label)
|
|
||||||
if windowTitle:
|
|
||||||
title = str(windowTitle, errors='replace')
|
|
||||||
user32.SetWindowTextW(hwnd, title)
|
|
||||||
if defaultLocation:
|
|
||||||
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
|
|
||||||
if location:
|
|
||||||
x, y = location
|
|
||||||
desktopRect = wintypes.RECT()
|
|
||||||
user32.GetWindowRect(0, ctypes.byref(desktopRect))
|
|
||||||
user32.SetWindowPos(hwnd, 0,
|
|
||||||
desktopRect.left + x,
|
|
||||||
desktopRect.top + y, 0, 0,
|
|
||||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
|
|
||||||
else:
|
|
||||||
CenterWindow(hwnd)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# This next line is needed to prevent gc of the callback
|
|
||||||
callback = BrowseCallbackProc(BrowseCallback)
|
|
||||||
|
|
||||||
browseInfo = BROWSEINFO()
|
|
||||||
browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
|
|
||||||
browseInfo.lpszTitle = message
|
|
||||||
browseInfo.lpfn = callback
|
|
||||||
|
|
||||||
pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
|
|
||||||
if not pidl:
|
|
||||||
result = None
|
|
||||||
else:
|
|
||||||
path = LPCWSTR(" " * (MAX_PATH+1))
|
|
||||||
shell32.SHGetPathFromIDListW(pidl, path)
|
|
||||||
ole32.CoTaskMemFree(pidl)
|
|
||||||
result = path.value
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
# Python 3, September 2020
|
# Python 3, September 2020
|
||||||
|
|
||||||
# Standard Python modules.
|
# Standard Python modules.
|
||||||
import sys, os, traceback, json, codecs, base64
|
import sys, os, traceback, json, codecs, base64, time
|
||||||
|
|
||||||
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||||
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
|
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
|
||||||
@@ -29,7 +29,7 @@ from calibre.constants import iswindows, isosx
|
|||||||
|
|
||||||
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
|
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||||
from __version import RESOURCE_NAME as help_file_name
|
from __version import RESOURCE_NAME as help_file_name
|
||||||
from utilities import uStrCmp
|
from .utilities import uStrCmp
|
||||||
|
|
||||||
import prefs
|
import prefs
|
||||||
import androidkindlekey
|
import androidkindlekey
|
||||||
@@ -1152,7 +1152,8 @@ class AddAdeptDialog():
|
|||||||
zip_function = zip
|
zip_function = zip
|
||||||
|
|
||||||
for key, name in zip_function(defaultkeys, defaultnames):
|
for key, name in zip_function(defaultkeys, defaultnames):
|
||||||
if codecs.encode(key,'hex').decode("latin-1") in self.parent.plugin_keys.values():
|
key = codecs.encode(key,'hex').decode("latin-1")
|
||||||
|
if key in self.parent.plugin_keys.values():
|
||||||
print("Found key '{0}' in ADE - already present, skipping.".format(name))
|
print("Found key '{0}' in ADE - already present, skipping.".format(name))
|
||||||
else:
|
else:
|
||||||
self.new_keys.append(key)
|
self.new_keys.append(key)
|
||||||
@@ -1167,8 +1168,8 @@ class AddAdeptDialog():
|
|||||||
key, name = checkForDeACSMkeys()
|
key, name = checkForDeACSMkeys()
|
||||||
|
|
||||||
if key is not None:
|
if key is not None:
|
||||||
|
key = codecs.encode(key,'hex').decode("latin-1")
|
||||||
if codecs.encode(key,'hex').decode("latin-1") in self.parent.plugin_keys.values():
|
if key in self.parent.plugin_keys.values():
|
||||||
print("Found key '{0}' in DeACSM - already present, skipping.".format(name))
|
print("Found key '{0}' in DeACSM - already present, skipping.".format(name))
|
||||||
else:
|
else:
|
||||||
# Found new key, add that.
|
# Found new key, add that.
|
||||||
@@ -1202,7 +1203,7 @@ class AddAdeptDialog():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def key_value(self):
|
def key_value(self):
|
||||||
return codecs.encode(self.new_keys[0],'hex').decode("utf-8")
|
return codecs.encode(self.new_keys[0],'hex').decode("latin-1")
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1237,7 +1238,7 @@ class AddKindleDialog(QDialog):
|
|||||||
from wineutils import WineGetKeys
|
from wineutils import WineGetKeys
|
||||||
|
|
||||||
scriptpath = os.path.join(parent.parent.alfdir,"kindlekey.py")
|
scriptpath = os.path.join(parent.parent.alfdir,"kindlekey.py")
|
||||||
defaultkeys = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix())
|
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix())
|
||||||
|
|
||||||
self.default_key = defaultkeys[0]
|
self.default_key = defaultkeys[0]
|
||||||
except:
|
except:
|
||||||
@@ -1255,7 +1256,7 @@ class AddKindleDialog(QDialog):
|
|||||||
key_group = QHBoxLayout()
|
key_group = QHBoxLayout()
|
||||||
data_group_box_layout.addLayout(key_group)
|
data_group_box_layout.addLayout(key_group)
|
||||||
key_group.addWidget(QLabel("Unique Key Name:", self))
|
key_group.addWidget(QLabel("Unique Key Name:", self))
|
||||||
self.key_ledit = QLineEdit("default_key", self)
|
self.key_ledit = QLineEdit("default_key_" + str(int(time.time())), self)
|
||||||
self.key_ledit.setToolTip("<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
|
self.key_ledit.setToolTip("<p>Enter an identifying name for the current default Kindle for Mac/PC key.")
|
||||||
key_group.addWidget(self.key_ledit)
|
key_group.addWidget(self.key_ledit)
|
||||||
|
|
||||||
@@ -1325,7 +1326,7 @@ class AddSerialDialog(QDialog):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def key_value(self):
|
def key_value(self):
|
||||||
return str(self.key_ledit.text()).replace(' ', '')
|
return str(self.key_ledit.text()).replace(' ', '').replace('\r', '').replace('\n', '').replace('\t', '')
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
if len(self.key_name) == 0 or self.key_name.isspace():
|
if len(self.key_name) == 0 or self.key_name.isspace():
|
||||||
|
|||||||
@@ -5,36 +5,16 @@
|
|||||||
# For use with Topaz Scripts Version 2.6
|
# For use with Topaz Scripts Version 2.6
|
||||||
# Python 3, September 2020
|
# Python 3, September 2020
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
from .utilities import SafeUnbuffered
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
import getopt
|
import getopt
|
||||||
from struct import pack
|
from struct import pack, unpack
|
||||||
from struct import unpack
|
|
||||||
|
|
||||||
class TpzDRMError(Exception):
|
class TpzDRMError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# epubfontdecrypt.py
|
# epubfontdecrypt.py
|
||||||
# Copyright © 2021 by noDRM
|
# Copyright © 2021-2023 by noDRM
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
# Revision history:
|
# Revision history:
|
||||||
# 1 - Initial release
|
# 1 - Initial release
|
||||||
|
# 2 - Bugfix for multiple book IDs, reported at #347
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypts / deobfuscates font files in EPUB files
|
Decrypts / deobfuscates font files in EPUB files
|
||||||
@@ -18,13 +19,14 @@ Decrypts / deobfuscates font files in EPUB files
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "1"
|
__version__ = "2"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from zeroedzipinfo import ZeroedZipInfo
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import itertools
|
import itertools
|
||||||
@@ -192,9 +194,10 @@ def decryptFontsBook(inpath, outpath):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
identify_element = container.find(packageNS("metadata")).find(metadataDCNS("identifier"))
|
identify_elements = container.find(packageNS("metadata")).findall(metadataDCNS("identifier"))
|
||||||
if (secret_key_name is None or secret_key_name == identify_element.get("id")):
|
for element in identify_elements:
|
||||||
font_master_key = identify_element.text
|
if (secret_key_name is None or secret_key_name == element.get("id")):
|
||||||
|
font_master_key = element.text
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -298,13 +301,21 @@ def decryptFontsBook(inpath, outpath):
|
|||||||
zi.internal_attr = oldzi.internal_attr
|
zi.internal_attr = oldzi.internal_attr
|
||||||
# external attributes are dependent on the create system, so copy both.
|
# external attributes are dependent on the create system, so copy both.
|
||||||
zi.external_attr = oldzi.external_attr
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.volume = oldzi.volume
|
||||||
zi.create_system = oldzi.create_system
|
zi.create_system = oldzi.create_system
|
||||||
|
zi.create_version = oldzi.create_version
|
||||||
|
|
||||||
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
zi.flag_bits |= 0x800
|
zi.flag_bits |= 0x800
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if zi.external_attr == 0:
|
||||||
|
zi = ZeroedZipInfo(zi)
|
||||||
|
|
||||||
if path == "mimetype":
|
if path == "mimetype":
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
elif path == "META-INF/encryption.xml":
|
elif path == "META-INF/encryption.xml":
|
||||||
|
|||||||
@@ -49,79 +49,19 @@
|
|||||||
|
|
||||||
__version__ = '2.0'
|
__version__ = '2.0'
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
import sys, struct, os, traceback
|
import sys, struct, os, traceback
|
||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
from .utilities import SafeUnbuffered
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
except:
|
|
||||||
iswindows = sys.platform.startswith('win')
|
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["epubtest.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
_FILENAME_LEN_OFFSET = 26
|
_FILENAME_LEN_OFFSET = 26
|
||||||
_EXTRA_LEN_OFFSET = 28
|
_EXTRA_LEN_OFFSET = 28
|
||||||
@@ -206,10 +146,8 @@ def encryption(infile):
|
|||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
if len(bookkey) == 172:
|
if len(bookkey) >= 172:
|
||||||
encryption = "Adobe (old)"
|
encryption = "Adobe"
|
||||||
if len(bookkey) == 192:
|
|
||||||
encryption = "Adobe (new)"
|
|
||||||
elif len(bookkey) == 64:
|
elif len(bookkey) == 64:
|
||||||
encryption = "B&N"
|
encryption = "B&N"
|
||||||
else:
|
else:
|
||||||
@@ -221,7 +159,7 @@ def encryption(infile):
|
|||||||
return encryption
|
return encryption
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("epubtest.py")
|
||||||
if len(argv) < 2:
|
if len(argv) < 2:
|
||||||
print("Give an ePub file as a parameter.")
|
print("Give an ePub file as a parameter.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Removes various watermarks from EPUB files
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from zeroedzipinfo import ZeroedZipInfo
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import re
|
import re
|
||||||
@@ -133,13 +134,22 @@ def removeHTMLwatermarks(object, path_to_ebook):
|
|||||||
zi.extra = oldzi.extra
|
zi.extra = oldzi.extra
|
||||||
zi.internal_attr = oldzi.internal_attr
|
zi.internal_attr = oldzi.internal_attr
|
||||||
zi.external_attr = oldzi.external_attr
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.volume = oldzi.volume
|
||||||
zi.create_system = oldzi.create_system
|
zi.create_system = oldzi.create_system
|
||||||
|
zi.create_version = oldzi.create_version
|
||||||
|
|
||||||
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
zi.flag_bits |= 0x800
|
zi.flag_bits |= 0x800
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if zi.external_attr == 0:
|
||||||
|
zi = ZeroedZipInfo(zi)
|
||||||
|
|
||||||
|
|
||||||
outf.writestr(zi, data)
|
outf.writestr(zi, data)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -249,13 +259,21 @@ def removeOPFwatermarks(object, path_to_ebook):
|
|||||||
zi.extra = oldzi.extra
|
zi.extra = oldzi.extra
|
||||||
zi.internal_attr = oldzi.internal_attr
|
zi.internal_attr = oldzi.internal_attr
|
||||||
zi.external_attr = oldzi.external_attr
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.volume = oldzi.volume
|
||||||
zi.create_system = oldzi.create_system
|
zi.create_system = oldzi.create_system
|
||||||
|
zi.create_version = oldzi.create_version
|
||||||
|
|
||||||
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
zi.flag_bits |= 0x800
|
zi.flag_bits |= 0x800
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if zi.external_attr == 0:
|
||||||
|
zi = ZeroedZipInfo(zi)
|
||||||
|
|
||||||
outf.writestr(zi, data)
|
outf.writestr(zi, data)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -301,13 +319,21 @@ def removeCDPwatermark(object, path_to_ebook):
|
|||||||
zi.extra = oldzi.extra
|
zi.extra = oldzi.extra
|
||||||
zi.internal_attr = oldzi.internal_attr
|
zi.internal_attr = oldzi.internal_attr
|
||||||
zi.external_attr = oldzi.external_attr
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.volume = oldzi.volume
|
||||||
zi.create_system = oldzi.create_system
|
zi.create_system = oldzi.create_system
|
||||||
|
zi.create_version = oldzi.create_version
|
||||||
|
|
||||||
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
zi.flag_bits |= 0x800
|
zi.flag_bits |= 0x800
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if zi.external_attr == 0:
|
||||||
|
zi = ZeroedZipInfo(zi)
|
||||||
|
|
||||||
outf.writestr(zi, data)
|
outf.writestr(zi, data)
|
||||||
|
|
||||||
print("Watermark: Successfully removed cdp.info watermark")
|
print("Watermark: Successfully removed cdp.info watermark")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# erdr2pml.py
|
# erdr2pml.py
|
||||||
# Copyright © 2008-2021 The Dark Reverser, Apprentice Harper, noDRM et al.
|
# Copyright © 2008-2022 The Dark Reverser, Apprentice Harper, noDRM et al.
|
||||||
#
|
#
|
||||||
# Changelog
|
# Changelog
|
||||||
#
|
#
|
||||||
@@ -65,114 +65,27 @@
|
|||||||
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||||
# 1.00 - Added Python 3 compatibility for calibre 5.0
|
# 1.00 - Added Python 3 compatibility for calibre 5.0
|
||||||
# 1.01 - Bugfixes for standalone version.
|
# 1.01 - Bugfixes for standalone version.
|
||||||
|
# 1.02 - Remove OpenSSL support; only use PyCryptodome
|
||||||
|
|
||||||
__version__='1.00'
|
__version__='1.02'
|
||||||
|
|
||||||
import sys, re
|
import sys, re
|
||||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
|
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback, hashlib
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.Cipher import DES
|
||||||
|
except ImportError:
|
||||||
|
from Crypto.Cipher import DES
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
from .utilities import SafeUnbuffered
|
||||||
# and also make sure that any unicode strings get
|
from .argv_utils import unicode_argv
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
iswindows = sys.platform.startswith('win')
|
||||||
isosx = sys.platform.startswith('darwin')
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["mobidedrm.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
Des = None
|
|
||||||
if iswindows:
|
|
||||||
# first try with pycrypto
|
|
||||||
import pycrypto_des
|
|
||||||
Des = pycrypto_des.load_pycrypto()
|
|
||||||
if Des == None:
|
|
||||||
# they try with openssl
|
|
||||||
import openssl_des
|
|
||||||
Des = openssl_des.load_libcrypto()
|
|
||||||
else:
|
|
||||||
# first try with openssl
|
|
||||||
import openssl_des
|
|
||||||
Des = openssl_des.load_libcrypto()
|
|
||||||
if Des == None:
|
|
||||||
# then try with pycrypto
|
|
||||||
import pycrypto_des
|
|
||||||
Des = pycrypto_des.load_pycrypto()
|
|
||||||
|
|
||||||
# if that did not work then use pure python implementation
|
|
||||||
# of DES and try to speed it up with Psycho
|
|
||||||
if Des == None:
|
|
||||||
import python_des
|
|
||||||
Des = python_des.Des
|
|
||||||
# Import Psyco if available
|
|
||||||
try:
|
|
||||||
# http://psyco.sourceforge.net
|
|
||||||
import psyco
|
|
||||||
psyco.full()
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
from hashlib import sha1
|
|
||||||
except ImportError:
|
|
||||||
# older Python release
|
|
||||||
import sha
|
|
||||||
sha1 = lambda s: sha.new(s)
|
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
@@ -229,14 +142,20 @@ def sanitizeFileName(name):
|
|||||||
|
|
||||||
def fixKey(key):
|
def fixKey(key):
|
||||||
def fixByte(b):
|
def fixByte(b):
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
b = ord(b)
|
||||||
|
|
||||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||||
return bytes([fixByte(a) for a in key])
|
return bytes(bytearray([fixByte(a) for a in key]))
|
||||||
|
|
||||||
def deXOR(text, sp, table):
|
def deXOR(text, sp, table):
|
||||||
r=''
|
r=b''
|
||||||
j = sp
|
j = sp
|
||||||
for i in range(len(text)):
|
for i in range(len(text)):
|
||||||
r += chr(ord(table[j]) ^ ord(text[i]))
|
if sys.version_info[0] == 2:
|
||||||
|
r += chr(ord(table[j]) ^ ord(text[i]))
|
||||||
|
else:
|
||||||
|
r += bytes(bytearray([table[j] ^ text[i]]))
|
||||||
j = j + 1
|
j = j + 1
|
||||||
if j == len(table):
|
if j == len(table):
|
||||||
j = 0
|
j = 0
|
||||||
@@ -253,7 +172,7 @@ class EreaderProcessor(object):
|
|||||||
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
||||||
data = self.section_reader(1)
|
data = self.section_reader(1)
|
||||||
self.data = data
|
self.data = data
|
||||||
des = Des(fixKey(data[0:8]))
|
des = DES.new(fixKey(data[0:8]), DES.MODE_ECB)
|
||||||
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
||||||
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
|
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
|
||||||
raise ValueError('incorrect eReader version (error 2)')
|
raise ValueError('incorrect eReader version (error 2)')
|
||||||
@@ -317,7 +236,7 @@ class EreaderProcessor(object):
|
|||||||
if (self.flags & reqd_flags) != reqd_flags:
|
if (self.flags & reqd_flags) != reqd_flags:
|
||||||
print("Flags: 0x%X" % self.flags)
|
print("Flags: 0x%X" % self.flags)
|
||||||
raise ValueError('incompatible eReader file')
|
raise ValueError('incompatible eReader file')
|
||||||
des = Des(fixKey(user_key))
|
des = DES.new(fixKey(user_key), DES.MODE_ECB)
|
||||||
if version == 259:
|
if version == 259:
|
||||||
if drm_sub_version != 7:
|
if drm_sub_version != 7:
|
||||||
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||||
@@ -336,7 +255,7 @@ class EreaderProcessor(object):
|
|||||||
encrypted_key = r[172:172+8]
|
encrypted_key = r[172:172+8]
|
||||||
encrypted_key_sha = r[56:56+20]
|
encrypted_key_sha = r[56:56+20]
|
||||||
self.content_key = des.decrypt(encrypted_key)
|
self.content_key = des.decrypt(encrypted_key)
|
||||||
if sha1(self.content_key).digest() != encrypted_key_sha:
|
if hashlib.sha1(self.content_key).digest() != encrypted_key_sha:
|
||||||
raise ValueError('Incorrect Name and/or Credit Card')
|
raise ValueError('Incorrect Name and/or Credit Card')
|
||||||
|
|
||||||
def getNumImages(self):
|
def getNumImages(self):
|
||||||
@@ -393,7 +312,7 @@ class EreaderProcessor(object):
|
|||||||
# return bkinfo
|
# return bkinfo
|
||||||
|
|
||||||
def getText(self):
|
def getText(self):
|
||||||
des = Des(fixKey(self.content_key))
|
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
|
||||||
r = b''
|
r = b''
|
||||||
for i in range(self.num_text_pages):
|
for i in range(self.num_text_pages):
|
||||||
logging.debug('get page %d', i)
|
logging.debug('get page %d', i)
|
||||||
@@ -406,7 +325,7 @@ class EreaderProcessor(object):
|
|||||||
sect = self.section_reader(self.first_footnote_page)
|
sect = self.section_reader(self.first_footnote_page)
|
||||||
fnote_ids = deXOR(sect, 0, self.xortable)
|
fnote_ids = deXOR(sect, 0, self.xortable)
|
||||||
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
|
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
|
||||||
des = Des(fixKey(self.content_key))
|
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
|
||||||
for i in range(1,self.num_footnote_pages):
|
for i in range(1,self.num_footnote_pages):
|
||||||
logging.debug('get footnotepage %d', i)
|
logging.debug('get footnotepage %d', i)
|
||||||
id_len = ord(fnote_ids[2])
|
id_len = ord(fnote_ids[2])
|
||||||
@@ -430,7 +349,7 @@ class EreaderProcessor(object):
|
|||||||
sect = self.section_reader(self.first_sidebar_page)
|
sect = self.section_reader(self.first_sidebar_page)
|
||||||
sbar_ids = deXOR(sect, 0, self.xortable)
|
sbar_ids = deXOR(sect, 0, self.xortable)
|
||||||
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
|
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
|
||||||
des = Des(fixKey(self.content_key))
|
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
|
||||||
for i in range(1,self.num_sidebar_pages):
|
for i in range(1,self.num_sidebar_pages):
|
||||||
id_len = ord(sbar_ids[2])
|
id_len = ord(sbar_ids[2])
|
||||||
id = sbar_ids[3:3+id_len]
|
id = sbar_ids[3:3+id_len]
|
||||||
@@ -539,7 +458,7 @@ def getuser_key(name,cc):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
print("eRdr2Pml v{0}. Copyright © 2009–2020 The Dark Reverser et al.".format(__version__))
|
print("eRdr2Pml v{0}. Copyright © 2009–2020 The Dark Reverser et al.".format(__version__))
|
||||||
|
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("erdr2pml.py")
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||||
except getopt.GetoptError as err:
|
except getopt.GetoptError as err:
|
||||||
|
|||||||
@@ -4,29 +4,9 @@
|
|||||||
# Python 3 for calibre 5.0
|
# Python 3 for calibre 5.0
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
from .utilities import SafeUnbuffered
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import csv
|
import csv
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ import os
|
|||||||
import base64
|
import base64
|
||||||
try:
|
try:
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
except:
|
except ImportError:
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
import hashlib
|
import hashlib
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
def unpad(data, padding=16):
|
||||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
|
||||||
|
|
||||||
def unpad(data):
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
if sys.version_info[0] == 2:
|
||||||
pad_len = ord(data[-1])
|
pad_len = ord(data[-1])
|
||||||
else:
|
else:
|
||||||
@@ -27,6 +23,10 @@ def unpad(data):
|
|||||||
|
|
||||||
return data[:-pad_len]
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
|
||||||
|
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||||
|
|
||||||
|
|
||||||
def dump_keys(path_to_adobe_folder):
|
def dump_keys(path_to_adobe_folder):
|
||||||
|
|
||||||
activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
|
activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
|
||||||
@@ -52,10 +52,13 @@ def dump_keys(path_to_adobe_folder):
|
|||||||
hashes = []
|
hashes = []
|
||||||
|
|
||||||
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
||||||
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
try:
|
||||||
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
||||||
hashes.append(base64.b64encode(cc_hash).decode("ascii"))
|
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
||||||
#print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
hashes.append(base64.b64encode(cc_hash).decode("ascii"))
|
||||||
|
#print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ignoblekeyGenPassHash.py
|
# ignoblekeyGenPassHash.py
|
||||||
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
|
# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al.
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
@@ -31,170 +31,34 @@
|
|||||||
# 2.7 - Work if TkInter is missing
|
# 2.7 - Work if TkInter is missing
|
||||||
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
|
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
|
||||||
# 3.0 - Added Python 3 compatibility for calibre 5.0
|
# 3.0 - Added Python 3 compatibility for calibre 5.0
|
||||||
|
# 3.1 - Remove OpenSSL support, only PyCryptodome is supported now
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "3.0"
|
__version__ = "3.1"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from Cryptodome.Cipher import AES
|
||||||
except:
|
except ImportError:
|
||||||
iswindows = sys.platform.startswith('win')
|
from Crypto.Cipher import AES
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
from .utilities import SafeUnbuffered
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["ignoblekeygen.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
class IGNOBLEError(Exception):
|
class IGNOBLEError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if iswindows:
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
|
|
||||||
if libcrypto is None:
|
|
||||||
raise IGNOBLEError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
|
||||||
('rounds', c_int)]
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey, iv):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
self._iv = iv
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
|
||||||
if rv == 0:
|
|
||||||
raise IGNOBLEError('AES encryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key, iv):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
|
||||||
|
|
||||||
def encrypt(self, data):
|
|
||||||
return self._aes.encrypt(data)
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = None
|
|
||||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
|
||||||
for loader in cryptolist:
|
|
||||||
try:
|
|
||||||
AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, IGNOBLEError):
|
|
||||||
pass
|
|
||||||
return AES
|
|
||||||
|
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
def normalize_name(name):
|
def normalize_name(name):
|
||||||
return ''.join(x for x in name.lower() if x != ' ')
|
return ''.join(x for x in name.lower() if x != ' ')
|
||||||
|
|
||||||
@@ -215,8 +79,7 @@ def generate_key(name, ccn):
|
|||||||
name_sha = hashlib.sha1(name).digest()[:16]
|
name_sha = hashlib.sha1(name).digest()[:16]
|
||||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||||
both_sha = hashlib.sha1(name + ccn).digest()
|
both_sha = hashlib.sha1(name + ccn).digest()
|
||||||
aes = AES(ccn_sha, name_sha)
|
crypt = AES.new(ccn_sha, AES.MODE_CBC, name_sha).encrypt(both_sha + (b'\x0c' * 0x0c))
|
||||||
crypt = aes.encrypt(both_sha + (b'\x0c' * 0x0c))
|
|
||||||
userkey = hashlib.sha1(crypt).digest()
|
userkey = hashlib.sha1(crypt).digest()
|
||||||
return base64.b64encode(userkey)
|
return base64.b64encode(userkey)
|
||||||
|
|
||||||
@@ -224,13 +87,8 @@ def generate_key(name, ccn):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("ignoblekeyGenPassHash.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
if AES is None:
|
|
||||||
print("%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
|
||||||
"separately. Read the top-of-script comment for details." % \
|
|
||||||
(progname,))
|
|
||||||
return 1
|
|
||||||
if len(argv) != 4:
|
if len(argv) != 4:
|
||||||
print("usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname))
|
print("usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname))
|
||||||
return 1
|
return 1
|
||||||
@@ -316,13 +174,6 @@ def gui_main():
|
|||||||
self.status['text'] = "Keyfile successfully generated"
|
self.status['text'] = "Keyfile successfully generated"
|
||||||
|
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
if AES is None:
|
|
||||||
root.withdraw()
|
|
||||||
tkinter.messagebox.showerror(
|
|
||||||
"Ignoble EPUB Keyfile Generator",
|
|
||||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
|
||||||
"separately. Read the top-of-script comment for details.")
|
|
||||||
return 1
|
|
||||||
root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||||
root.resizable(True, False)
|
root.resizable(True, False)
|
||||||
root.minsize(300, 0)
|
root.minsize(300, 0)
|
||||||
|
|||||||
@@ -27,71 +27,16 @@ import hashlib
|
|||||||
import getopt
|
import getopt
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
from .utilities import SafeUnbuffered
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows
|
||||||
except:
|
except:
|
||||||
iswindows = sys.platform.startswith('win')
|
iswindows = sys.platform.startswith('win')
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
from .argv_utils import unicode_argv
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["ignoblekey.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -111,15 +56,26 @@ def getNookLogFiles():
|
|||||||
paths = set()
|
paths = set()
|
||||||
if 'LOCALAPPDATA' in os.environ.keys():
|
if 'LOCALAPPDATA' in os.environ.keys():
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
if sys.version_info[0] == 2:
|
||||||
|
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
|
||||||
|
else:
|
||||||
|
path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
paths.add(path)
|
paths.add(path)
|
||||||
if 'USERPROFILE' in os.environ.keys():
|
if 'USERPROFILE' in os.environ.keys():
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local"
|
if sys.version_info[0] == 2:
|
||||||
|
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
|
||||||
|
else:
|
||||||
|
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local"
|
||||||
|
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
paths.add(path)
|
paths.add(path)
|
||||||
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming"
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
|
||||||
|
else:
|
||||||
|
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming"
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
paths.add(path)
|
paths.add(path)
|
||||||
# User Shell Folders show take precedent over Shell Folders if present
|
# User Shell Folders show take precedent over Shell Folders if present
|
||||||
@@ -157,7 +113,7 @@ def getNookLogFiles():
|
|||||||
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
|
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
|
||||||
if os.path.isfile(logpath):
|
if os.path.isfile(logpath):
|
||||||
found = True
|
found = True
|
||||||
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'), file=sys.stderr)
|
print('Found nookStudy log file: ' + logpath, file=sys.stderr)
|
||||||
logFiles.append(logpath)
|
logFiles.append(logpath)
|
||||||
else:
|
else:
|
||||||
home = os.getenv('HOME')
|
home = os.getenv('HOME')
|
||||||
@@ -244,7 +200,7 @@ def usage(progname):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("ignoblekeyNookStudy.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__))
|
print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__))
|
||||||
|
|
||||||
@@ -305,7 +261,7 @@ def gui_main():
|
|||||||
self.text.insert(tkinter.constants.END, text)
|
self.text.insert(tkinter.constants.END, text)
|
||||||
|
|
||||||
|
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("ignoblekeyNookStudy.py")
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
progpath, progname = os.path.split(argv[0])
|
progpath, progname = os.path.split(argv[0])
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ https://github.com/noDRM/DeDRM_tools/discussions/9
|
|||||||
import sys, os
|
import sys, os
|
||||||
import apsw
|
import apsw
|
||||||
import base64
|
import base64
|
||||||
|
import traceback
|
||||||
try:
|
try:
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
except:
|
except:
|
||||||
@@ -20,12 +21,7 @@ except:
|
|||||||
import hashlib
|
import hashlib
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
def unpad(data, padding=16):
|
||||||
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
|
|
||||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
|
||||||
|
|
||||||
def unpad(data):
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
if sys.version_info[0] == 2:
|
||||||
pad_len = ord(data[-1])
|
pad_len = ord(data[-1])
|
||||||
else:
|
else:
|
||||||
@@ -34,6 +30,10 @@ def unpad(data):
|
|||||||
return data[:-pad_len]
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
|
||||||
|
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
|
||||||
|
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||||
|
|
||||||
|
|
||||||
def dump_keys(print_result=False):
|
def dump_keys(print_result=False):
|
||||||
db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
|
db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
|
||||||
|
|
||||||
@@ -63,11 +63,14 @@ def dump_keys(print_result=False):
|
|||||||
decrypted_hashes = []
|
decrypted_hashes = []
|
||||||
|
|
||||||
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
||||||
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
try:
|
||||||
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
||||||
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
|
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16)
|
||||||
if print_result:
|
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
|
||||||
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
if print_result:
|
||||||
|
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
return decrypted_hashes
|
return decrypted_hashes
|
||||||
|
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# ignoblekeyfetch.py
|
|
||||||
# Copyright © 2015-2020 Apprentice Harper et al.
|
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
|
||||||
# <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
# Based on discoveries by "Nobody You Know"
|
|
||||||
# Code partly based on ignoblekeygen.py by several people.
|
|
||||||
|
|
||||||
# Windows users: Before running this program, you must first install Python.
|
|
||||||
# We recommend ActiveState Python 2.7.X for Windows from
|
|
||||||
# http://www.activestate.com/activepython/downloads.
|
|
||||||
# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
|
|
||||||
#
|
|
||||||
# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
|
|
||||||
# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
|
|
||||||
# it when it has been associated with PythonLauncher.
|
|
||||||
|
|
||||||
# Revision history:
|
|
||||||
# 1.0 - Initial version
|
|
||||||
# 1.1 - Try second URL if first one fails
|
|
||||||
# 2.0 - Python 3 for calibre 5.0
|
|
||||||
|
|
||||||
"""
|
|
||||||
Fetch Barnes & Noble EPUB user key from B&N servers using email and password.
|
|
||||||
|
|
||||||
NOTE: This script used to work in the past, but the server it uses is long gone.
|
|
||||||
It can no longer be used to download keys from B&N servers, it is no longer
|
|
||||||
supported by the Calibre plugin, and it will be removed in the future.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__version__ = "2.0"
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
except:
|
|
||||||
iswindows = sys.platform.startswith('win')
|
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
|
||||||
# as a list of Unicode strings and encode them as utf-8
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["ignoblekeyfetch.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
|
|
||||||
class IGNOBLEError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fetch_key(email, password):
|
|
||||||
# change email and password to utf-8 if unicode
|
|
||||||
if type(email)==str:
|
|
||||||
email = email.encode('utf-8')
|
|
||||||
if type(password)==str:
|
|
||||||
password = password.encode('utf-8')
|
|
||||||
|
|
||||||
import random
|
|
||||||
random = "%030x" % random.randrange(16**30)
|
|
||||||
|
|
||||||
import urllib.parse, urllib.request, re
|
|
||||||
|
|
||||||
# try the URL from nook for PC
|
|
||||||
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
|
|
||||||
fetch_url += urllib.parse.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
|
|
||||||
fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
|
|
||||||
#print fetch_url
|
|
||||||
|
|
||||||
found = ''
|
|
||||||
try:
|
|
||||||
response = urllib.request.urlopen(fetch_url)
|
|
||||||
the_page = response.read()
|
|
||||||
#print the_page
|
|
||||||
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
|
|
||||||
except:
|
|
||||||
found = ''
|
|
||||||
if len(found)!=28:
|
|
||||||
# try the URL from android devices
|
|
||||||
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
|
|
||||||
fetch_url += urllib.parse.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
|
|
||||||
fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
|
|
||||||
#print fetch_url
|
|
||||||
|
|
||||||
found = ''
|
|
||||||
try:
|
|
||||||
response = urllib.request.urlopen(fetch_url)
|
|
||||||
the_page = response.read()
|
|
||||||
#print the_page
|
|
||||||
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
|
|
||||||
except:
|
|
||||||
found = ''
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cli_main():
|
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
|
||||||
argv=unicode_argv()
|
|
||||||
progname = os.path.basename(argv[0])
|
|
||||||
if len(argv) != 4:
|
|
||||||
print("usage: {0} <email> <password> <keyfileout.b64>".format(progname))
|
|
||||||
return 1
|
|
||||||
email, password, keypath = argv[1:]
|
|
||||||
userkey = fetch_key(email, password)
|
|
||||||
if len(userkey) == 28:
|
|
||||||
open(keypath,'wb').write(userkey)
|
|
||||||
return 0
|
|
||||||
print("Failed to fetch key.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
|
||||||
try:
|
|
||||||
import tkinter
|
|
||||||
import tkinter.filedialog
|
|
||||||
import tkinter.constants
|
|
||||||
import tkinter.messagebox
|
|
||||||
import traceback
|
|
||||||
except:
|
|
||||||
return cli_main()
|
|
||||||
|
|
||||||
class DecryptionDialog(tkinter.Frame):
|
|
||||||
def __init__(self, root):
|
|
||||||
tkinter.Frame.__init__(self, root, border=5)
|
|
||||||
self.status = tkinter.Label(self, text="Enter parameters")
|
|
||||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
|
||||||
body = tkinter.Frame(self)
|
|
||||||
body.pack(fill=tkinter.constants.X, expand=1)
|
|
||||||
sticky = tkinter.constants.E + tkinter.constants.W
|
|
||||||
body.grid_columnconfigure(1, weight=2)
|
|
||||||
tkinter.Label(body, text="Account email address").grid(row=0)
|
|
||||||
self.name = tkinter.Entry(body, width=40)
|
|
||||||
self.name.grid(row=0, column=1, sticky=sticky)
|
|
||||||
tkinter.Label(body, text="Account password").grid(row=1)
|
|
||||||
self.ccn = tkinter.Entry(body, width=40)
|
|
||||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
|
||||||
tkinter.Label(body, text="Output file").grid(row=2)
|
|
||||||
self.keypath = tkinter.Entry(body, width=40)
|
|
||||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
|
||||||
self.keypath.insert(2, "bnepubkey.b64")
|
|
||||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
|
||||||
button.grid(row=2, column=2)
|
|
||||||
buttons = tkinter.Frame(self)
|
|
||||||
buttons.pack()
|
|
||||||
botton = tkinter.Button(
|
|
||||||
buttons, text="Fetch", width=10, command=self.generate)
|
|
||||||
botton.pack(side=tkinter.constants.LEFT)
|
|
||||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
|
||||||
button = tkinter.Button(
|
|
||||||
buttons, text="Quit", width=10, command=self.quit)
|
|
||||||
button.pack(side=tkinter.constants.RIGHT)
|
|
||||||
|
|
||||||
def get_keypath(self):
|
|
||||||
keypath = tkinter.filedialog.asksaveasfilename(
|
|
||||||
parent=None, title="Select B&N ePub key file to produce",
|
|
||||||
defaultextension=".b64",
|
|
||||||
filetypes=[('base64-encoded files', '.b64'),
|
|
||||||
('All Files', '.*')])
|
|
||||||
if keypath:
|
|
||||||
keypath = os.path.normpath(keypath)
|
|
||||||
self.keypath.delete(0, tkinter.constants.END)
|
|
||||||
self.keypath.insert(0, keypath)
|
|
||||||
return
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
email = self.name.get()
|
|
||||||
password = self.ccn.get()
|
|
||||||
keypath = self.keypath.get()
|
|
||||||
if not email:
|
|
||||||
self.status['text'] = "Email address not given"
|
|
||||||
return
|
|
||||||
if not password:
|
|
||||||
self.status['text'] = "Account password not given"
|
|
||||||
return
|
|
||||||
if not keypath:
|
|
||||||
self.status['text'] = "Output keyfile path not set"
|
|
||||||
return
|
|
||||||
self.status['text'] = "Fetching..."
|
|
||||||
try:
|
|
||||||
userkey = fetch_key(email, password)
|
|
||||||
except Exception as e:
|
|
||||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
|
||||||
return
|
|
||||||
if len(userkey) == 28:
|
|
||||||
open(keypath,'wb').write(userkey)
|
|
||||||
self.status['text'] = "Keyfile fetched successfully"
|
|
||||||
else:
|
|
||||||
self.status['text'] = "Keyfile fetch failed."
|
|
||||||
|
|
||||||
root = tkinter.Tk()
|
|
||||||
root.title("Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
|
|
||||||
root.resizable(True, False)
|
|
||||||
root.minsize(300, 0)
|
|
||||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
|
||||||
root.mainloop()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
sys.exit(cli_main())
|
|
||||||
sys.exit(gui_main())
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptepub.py
|
# ineptepub.py
|
||||||
# Copyright © 2009-2021 by i♥cabbages, Apprentice Harper et al.
|
# Copyright © 2009-2022 by i♥cabbages, Apprentice Harper et al.
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
@@ -31,13 +31,15 @@
|
|||||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||||
# 7.0 - Add Python 3 compatibility for calibre 5.0
|
# 7.0 - Add Python 3 compatibility for calibre 5.0
|
||||||
# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
|
# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
|
||||||
|
# 7.2 - Only support PyCryptodome; clean up the code
|
||||||
|
# 8.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "7.1"
|
__version__ = "8.0"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -46,72 +48,32 @@ import base64
|
|||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from zeroedzipinfo import ZeroedZipInfo
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from uuid import UUID
|
||||||
# Wrap a stream so that output gets flushed immediately
|
import hashlib
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from Cryptodome.Cipher import AES, PKCS1_v1_5
|
||||||
except:
|
from Cryptodome.PublicKey import RSA
|
||||||
iswindows = sys.platform.startswith('win')
|
except ImportError:
|
||||||
isosx = sys.platform.startswith('darwin')
|
from Crypto.Cipher import AES, PKCS1_v1_5
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
def unpad(data, padding=16):
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
if sys.version_info[0] == 2:
|
||||||
|
pad_len = ord(data[-1])
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
return ["ineptepub.py"]
|
|
||||||
else:
|
else:
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
pad_len = data[-1]
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
from .utilities import SafeUnbuffered
|
||||||
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
class ADEPTError(Exception):
|
||||||
@@ -120,234 +82,6 @@ class ADEPTError(Exception):
|
|||||||
class ADEPTNewVersionError(Exception):
|
class ADEPTNewVersionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if iswindows:
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
|
|
||||||
if libcrypto is None:
|
|
||||||
raise ADEPTError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
RSA_NO_PADDING = 3
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
class RSA(Structure):
|
|
||||||
pass
|
|
||||||
RSA_p = POINTER(RSA)
|
|
||||||
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
|
||||||
('rounds', c_int)]
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
|
||||||
[RSA_p, c_char_pp, c_long])
|
|
||||||
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
|
||||||
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
|
||||||
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
|
||||||
RSA_free = F(None, 'RSA_free', [RSA_p])
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
|
||||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
|
||||||
c_int])
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
buf = create_string_buffer(der)
|
|
||||||
pp = c_char_pp(cast(buf, c_char_p))
|
|
||||||
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
|
||||||
if rsa is None:
|
|
||||||
raise ADEPTError('Error parsing ADEPT user key DER')
|
|
||||||
|
|
||||||
def decrypt(self, from_):
|
|
||||||
rsa = self._rsa
|
|
||||||
to = create_string_buffer(RSA_size(rsa))
|
|
||||||
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
|
||||||
RSA_NO_PADDING)
|
|
||||||
if dlen < 0:
|
|
||||||
raise ADEPTError('RSA decryption failed')
|
|
||||||
return to[:dlen]
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if self._rsa is not None:
|
|
||||||
RSA_free(self._rsa)
|
|
||||||
self._rsa = None
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise ADEPTError('AES improper key used')
|
|
||||||
return
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise ADEPTError('Failed to initialize AES key')
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
iv = (b"\x00" * self._blocksize)
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise ADEPTError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
return (AES, RSA)
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
try:
|
|
||||||
from Cryptodome.Cipher import AES as _AES
|
|
||||||
from Cryptodome.PublicKey import RSA as _RSA
|
|
||||||
from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
|
||||||
except:
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
from Crypto.PublicKey import RSA as _RSA
|
|
||||||
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
|
||||||
|
|
||||||
# ASN.1 parsing code from tlslite
|
|
||||||
class ASN1Error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ASN1Parser(object):
|
|
||||||
class Parser(object):
|
|
||||||
def __init__(self, bytes):
|
|
||||||
self.bytes = bytes
|
|
||||||
self.index = 0
|
|
||||||
|
|
||||||
def get(self, length):
|
|
||||||
if self.index + length > len(self.bytes):
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
x = 0
|
|
||||||
for count in range(length):
|
|
||||||
x <<= 8
|
|
||||||
x |= self.bytes[self.index]
|
|
||||||
self.index += 1
|
|
||||||
return x
|
|
||||||
|
|
||||||
def getFixBytes(self, lengthBytes):
|
|
||||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
|
||||||
self.index += lengthBytes
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def getVarBytes(self, lengthLength):
|
|
||||||
lengthBytes = self.get(lengthLength)
|
|
||||||
return self.getFixBytes(lengthBytes)
|
|
||||||
|
|
||||||
def getFixList(self, length, lengthList):
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def getVarList(self, length, lengthLength):
|
|
||||||
lengthList = self.get(lengthLength)
|
|
||||||
if lengthList % length != 0:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
lengthList = int(lengthList/length)
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def startLengthCheck(self, lengthLength):
|
|
||||||
self.lengthCheck = self.get(lengthLength)
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def setLengthCheck(self, length):
|
|
||||||
self.lengthCheck = length
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def stopLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def atLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
|
||||||
return False
|
|
||||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def __init__(self, bytes):
|
|
||||||
p = self.Parser(bytes)
|
|
||||||
p.get(1)
|
|
||||||
self.length = self._getASN1Length(p)
|
|
||||||
self.value = p.getFixBytes(self.length)
|
|
||||||
|
|
||||||
def getChild(self, which):
|
|
||||||
p = self.Parser(self.value)
|
|
||||||
for x in range(which+1):
|
|
||||||
markIndex = p.index
|
|
||||||
p.get(1)
|
|
||||||
length = self._getASN1Length(p)
|
|
||||||
p.getFixBytes(length)
|
|
||||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
|
||||||
|
|
||||||
def _getASN1Length(self, p):
|
|
||||||
firstLength = p.get(1)
|
|
||||||
if firstLength<=127:
|
|
||||||
return firstLength
|
|
||||||
else:
|
|
||||||
lengthLength = firstLength & 0x7F
|
|
||||||
return p.get(lengthLength)
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
key = ASN1Parser([x for x in der])
|
|
||||||
key = [key.getChild(x).value for x in range(1, 4)]
|
|
||||||
key = [self.bytesToNumber(v) for v in key]
|
|
||||||
self._rsa = _RSA.construct(key)
|
|
||||||
|
|
||||||
def bytesToNumber(self, bytes):
|
|
||||||
total = 0
|
|
||||||
for byte in bytes:
|
|
||||||
total = (total << 8) + byte
|
|
||||||
return total
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
|
|
||||||
|
|
||||||
return (AES, RSA)
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = RSA = None
|
|
||||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
|
||||||
for loader in cryptolist:
|
|
||||||
try:
|
|
||||||
AES, RSA = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, ADEPTError):
|
|
||||||
pass
|
|
||||||
return (AES, RSA)
|
|
||||||
|
|
||||||
AES, RSA = _load_crypto()
|
|
||||||
|
|
||||||
META_NAMES = ('mimetype', 'META-INF/rights.xml')
|
META_NAMES = ('mimetype', 'META-INF/rights.xml')
|
||||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
@@ -355,16 +89,17 @@ NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
|||||||
class Decryptor(object):
|
class Decryptor(object):
|
||||||
def __init__(self, bookkey, encryption):
|
def __init__(self, bookkey, encryption):
|
||||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||||
self._aes = AES(bookkey)
|
self._aes = AES.new(bookkey, AES.MODE_CBC, b'\x00'*16)
|
||||||
encryption = etree.fromstring(encryption)
|
self._encryption = etree.fromstring(encryption)
|
||||||
self._encrypted = encrypted = set()
|
self._encrypted = encrypted = set()
|
||||||
|
self._encryptedForceNoDecomp = encryptedForceNoDecomp = set()
|
||||||
self._otherData = otherData = set()
|
self._otherData = otherData = set()
|
||||||
|
|
||||||
self._json_elements_to_remove = json_elements_to_remove = set()
|
self._json_elements_to_remove = json_elements_to_remove = set()
|
||||||
self._has_remaining_xml = False
|
self._has_remaining_xml = False
|
||||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||||
enc('CipherReference'))
|
enc('CipherReference'))
|
||||||
for elem in encryption.findall(expr):
|
for elem in self._encryption.findall(expr):
|
||||||
path = elem.get('URI', None)
|
path = elem.get('URI', None)
|
||||||
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
|
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
|
||||||
if path is not None:
|
if path is not None:
|
||||||
@@ -373,6 +108,11 @@ class Decryptor(object):
|
|||||||
path = path.encode('utf-8')
|
path = path.encode('utf-8')
|
||||||
encrypted.add(path)
|
encrypted.add(path)
|
||||||
json_elements_to_remove.add(elem.getparent().getparent())
|
json_elements_to_remove.add(elem.getparent().getparent())
|
||||||
|
elif (encryption_type_url == "http://ns.adobe.com/adept/xmlenc#aes128-cbc-uncompressed"):
|
||||||
|
# Adobe uncompressed, for stuff like video files
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
encryptedForceNoDecomp.add(path)
|
||||||
|
json_elements_to_remove.add(elem.getparent().getparent())
|
||||||
else:
|
else:
|
||||||
path = path.encode('utf-8')
|
path = path.encode('utf-8')
|
||||||
otherData.add(path)
|
otherData.add(path)
|
||||||
@@ -401,14 +141,15 @@ class Decryptor(object):
|
|||||||
return decompressed_bytes
|
return decompressed_bytes
|
||||||
|
|
||||||
def decrypt(self, path, data):
|
def decrypt(self, path, data):
|
||||||
if path.encode('utf-8') in self._encrypted:
|
if path.encode('utf-8') in self._encrypted or path.encode('utf-8') in self._encryptedForceNoDecomp:
|
||||||
data = self._aes.decrypt(data)[16:]
|
data = self._aes.decrypt(data)[16:]
|
||||||
if type(data[-1]) != int:
|
if type(data[-1]) != int:
|
||||||
place = ord(data[-1])
|
place = ord(data[-1])
|
||||||
else:
|
else:
|
||||||
place = data[-1]
|
place = data[-1]
|
||||||
data = data[:-place]
|
data = data[:-place]
|
||||||
data = self.decompress(data)
|
if not path.encode('utf-8') in self._encryptedForceNoDecomp:
|
||||||
|
data = self.decompress(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||||
@@ -465,30 +206,24 @@ def adeptGetUserUUID(inpath):
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def verify_book_key(bookkey):
|
def removeHardening(rights, keytype, keydata):
|
||||||
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
# Byte not null, invalid result
|
textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
|
||||||
return False
|
|
||||||
|
|
||||||
if ((bookkey[0] != '\x02' and bookkey[0] != 2) and
|
# Gather what we need, and generate the IV
|
||||||
((bookkey[0] != '\x00' and bookkey[0] != 0) or
|
resourceuuid = UUID(textGetter("resource"))
|
||||||
(bookkey[1] != '\x02' and bookkey[1] != 2))):
|
deviceuuid = UUID(textGetter("device"))
|
||||||
# Key not starting with "00 02" or "02" -> error
|
fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
|
||||||
return False
|
kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
|
||||||
|
|
||||||
keylen = len(bookkey) - 17
|
# Derive kek from just "keytype"
|
||||||
for i in range(1, keylen):
|
rem = int(keytype, 10) % 16
|
||||||
if bookkey[i] == 0 or bookkey[i] == '\x00':
|
H = hashlib.sha256(keytype.encode("ascii")).digest()
|
||||||
# Padding data contains a space - that's not allowed.
|
kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
|
||||||
# Probably bad decryption.
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7
|
||||||
|
|
||||||
def decryptBook(userkey, inpath, outpath):
|
def decryptBook(userkey, inpath, outpath):
|
||||||
if AES is None:
|
|
||||||
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
|
|
||||||
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
namelist = inf.namelist()
|
namelist = inf.namelist()
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
@@ -501,15 +236,12 @@ def decryptBook(userkey, inpath, outpath):
|
|||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
bookkeyelem = rights.find(expr)
|
||||||
if len(bookkey) == 192:
|
bookkey = bookkeyelem.text
|
||||||
print("{0:s} seems to be an Adobe ADEPT ePub with Adobe's new DRM".format(os.path.basename(inpath)))
|
keytype = bookkeyelem.attrib.get('keyType', '0')
|
||||||
print("This DRM cannot be removed yet. ")
|
if len(bookkey) >= 172 and int(keytype, 10) > 2:
|
||||||
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
|
print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath)))
|
||||||
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
|
elif len(bookkey) == 172:
|
||||||
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
|
|
||||||
|
|
||||||
if len(bookkey) == 172:
|
|
||||||
print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
||||||
elif len(bookkey) == 64:
|
elif len(bookkey) == 64:
|
||||||
print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
|
print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
|
||||||
@@ -518,29 +250,24 @@ def decryptBook(userkey, inpath, outpath):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
if len(bookkey) != 64:
|
if len(bookkey) != 64:
|
||||||
# Normal Adobe ADEPT
|
# Normal or "hardened" Adobe ADEPT
|
||||||
rsa = RSA(userkey)
|
rsakey = RSA.importKey(userkey) # parses the ASN1 structure
|
||||||
bookkey = rsa.decrypt(base64.b64decode(bookkey.encode('ascii')))
|
bookkey = base64.b64decode(bookkey)
|
||||||
|
if int(keytype, 10) > 2:
|
||||||
|
bookkey = removeHardening(rights, keytype, bookkey)
|
||||||
|
try:
|
||||||
|
bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
|
||||||
|
except ValueError:
|
||||||
|
bookkey = None
|
||||||
|
|
||||||
# Verify key:
|
if bookkey is None:
|
||||||
if len(bookkey) > 16:
|
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
return 2
|
||||||
if verify_book_key(bookkey):
|
|
||||||
bookkey = bookkey[-16:]
|
|
||||||
else:
|
|
||||||
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
|
|
||||||
return 2
|
|
||||||
else:
|
else:
|
||||||
# Adobe PassHash / B&N
|
# Adobe PassHash / B&N
|
||||||
key = base64.b64decode(userkey)[:16]
|
key = base64.b64decode(userkey)[:16]
|
||||||
aes = AES(key)
|
bookkey = base64.b64decode(bookkey)
|
||||||
bookkey = aes.decrypt(base64.b64decode(bookkey))
|
bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7
|
||||||
if type(bookkey[-1]) != int:
|
|
||||||
pad = ord(bookkey[-1])
|
|
||||||
else:
|
|
||||||
pad = bookkey[-1]
|
|
||||||
|
|
||||||
bookkey = bookkey[:-pad]
|
|
||||||
|
|
||||||
if len(bookkey) > 16:
|
if len(bookkey) > 16:
|
||||||
bookkey = bookkey[-16:]
|
bookkey = bookkey[-16:]
|
||||||
@@ -578,12 +305,23 @@ def decryptBook(userkey, inpath, outpath):
|
|||||||
zi.internal_attr = oldzi.internal_attr
|
zi.internal_attr = oldzi.internal_attr
|
||||||
# external attributes are dependent on the create system, so copy both.
|
# external attributes are dependent on the create system, so copy both.
|
||||||
zi.external_attr = oldzi.external_attr
|
zi.external_attr = oldzi.external_attr
|
||||||
|
|
||||||
|
zi.volume = oldzi.volume
|
||||||
zi.create_system = oldzi.create_system
|
zi.create_system = oldzi.create_system
|
||||||
|
zi.create_version = oldzi.create_version
|
||||||
|
|
||||||
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
zi.flag_bits |= 0x800
|
zi.flag_bits |= 0x800
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if zi.external_attr == 0:
|
||||||
|
zi = ZeroedZipInfo(zi)
|
||||||
|
|
||||||
|
|
||||||
if path == "META-INF/encryption.xml":
|
if path == "META-INF/encryption.xml":
|
||||||
outf.writestr(zi, data)
|
outf.writestr(zi, data)
|
||||||
else:
|
else:
|
||||||
@@ -597,7 +335,7 @@ def decryptBook(userkey, inpath, outpath):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("ineptepub.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
if len(argv) != 4:
|
if len(argv) != 4:
|
||||||
print("usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname))
|
print("usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# ineptpdf.py
|
# ineptpdf.py
|
||||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||||
# Copyright © 2021 by noDRM
|
# Copyright © 2021-2022 by noDRM et al.
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
# <http://www.gnu.org/licenses/>
|
# <http://www.gnu.org/licenses/>
|
||||||
@@ -48,88 +48,58 @@
|
|||||||
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
||||||
# 9.0.0 - Add Python 3 compatibility for calibre 5
|
# 9.0.0 - Add Python 3 compatibility for calibre 5
|
||||||
# 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
|
# 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
|
||||||
|
# 9.1.1 - Only support PyCryptodome; clean up the code
|
||||||
|
# 10.0.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
|
||||||
|
# 10.0.2 - Fix some Python2 stuff
|
||||||
|
# 10.0.4 - Fix more Python2 stuff
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "9.1.0"
|
__version__ = "10.0.4"
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import hashlib
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import zlib
|
import zlib
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
import binascii
|
||||||
|
import base64
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import itertools
|
import itertools
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
import traceback
|
import traceback
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
try:
|
||||||
# and also make sure that any unicode strings get
|
from Cryptodome.Cipher import AES, ARC4, PKCS1_v1_5
|
||||||
# encoded using "replace" before writing them.
|
from Cryptodome.PublicKey import RSA
|
||||||
class SafeUnbuffered:
|
except ImportError:
|
||||||
def __init__(self, stream):
|
from Crypto.Cipher import AES, ARC4, PKCS1_v1_5
|
||||||
self.stream = stream
|
from Crypto.PublicKey import RSA
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
def unpad(data, padding=16):
|
||||||
def write(self, data):
|
if sys.version_info[0] == 2:
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
pad_len = ord(data[-1])
|
||||||
# str for Python3, unicode for Python2
|
else:
|
||||||
data = data.encode(self.encoding,"replace")
|
pad_len = data[-1]
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
return data[:-pad_len]
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
buffer.flush()
|
|
||||||
except:
|
from .utilities import SafeUnbuffered
|
||||||
# We can do nothing if a write fails
|
from .argv_utils import unicode_argv
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
iswindows = sys.platform.startswith('win')
|
||||||
isosx = sys.platform.startswith('darwin')
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
return ["ineptpdf.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
|
|
||||||
class ADEPTError(Exception):
|
class ADEPTError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -140,320 +110,15 @@ class ADEPTInvalidPasswordError(Exception):
|
|||||||
class ADEPTNewVersionError(Exception):
|
class ADEPTNewVersionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
def SHA256(message):
|
def SHA256(message):
|
||||||
ctx = hashlib.sha256()
|
return hashlib.sha256(message).digest()
|
||||||
ctx.update(message)
|
|
||||||
return ctx.digest()
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
|
|
||||||
if libcrypto is None:
|
|
||||||
raise ADEPTError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
RSA_NO_PADDING = 3
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
|
|
||||||
class RC4_KEY(Structure):
|
|
||||||
_fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)]
|
|
||||||
RC4_KEY_p = POINTER(RC4_KEY)
|
|
||||||
|
|
||||||
class RSA(Structure):
|
|
||||||
pass
|
|
||||||
RSA_p = POINTER(RSA)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',[c_char_p, c_int, AES_KEY_p])
|
|
||||||
|
|
||||||
RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p])
|
|
||||||
RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p])
|
|
||||||
|
|
||||||
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
|
||||||
[RSA_p, c_char_pp, c_long])
|
|
||||||
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
|
||||||
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
|
||||||
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
|
||||||
RSA_free = F(None, 'RSA_free', [RSA_p])
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
buf = create_string_buffer(der)
|
|
||||||
pp = c_char_pp(cast(buf, c_char_p))
|
|
||||||
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
|
||||||
if rsa is None:
|
|
||||||
raise ADEPTError('Error parsing ADEPT user key DER')
|
|
||||||
|
|
||||||
def decrypt(self, from_):
|
|
||||||
rsa = self._rsa
|
|
||||||
to = create_string_buffer(RSA_size(rsa))
|
|
||||||
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
|
||||||
RSA_NO_PADDING)
|
|
||||||
if dlen < 0:
|
|
||||||
raise ADEPTError('RSA decryption failed')
|
|
||||||
return to[1:dlen]
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if self._rsa is not None:
|
|
||||||
RSA_free(self._rsa)
|
|
||||||
self._rsa = None
|
|
||||||
|
|
||||||
class ARC4(object):
|
|
||||||
@classmethod
|
|
||||||
def new(cls, userkey):
|
|
||||||
self = ARC4()
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
key = self._key = RC4_KEY()
|
|
||||||
RC4_set_key(key, self._blocksize, userkey)
|
|
||||||
return self
|
|
||||||
def __init__(self):
|
|
||||||
self._blocksize = 0
|
|
||||||
self._key = None
|
|
||||||
def decrypt(self, data):
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
RC4_crypt(self._key, len(data), data, out)
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
MODE_CBC = 0
|
|
||||||
@classmethod
|
|
||||||
def new(cls, userkey, mode, iv, decrypt=True):
|
|
||||||
self = AES()
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
# mode is ignored since CBCMODE is only thing supported/used so far
|
|
||||||
self._mode = mode
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise ADEPTError('AES improper key used')
|
|
||||||
return
|
|
||||||
keyctx = self._keyctx = AES_KEY()
|
|
||||||
self._iv = iv
|
|
||||||
self._isDecrypt = decrypt
|
|
||||||
if decrypt:
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
|
||||||
else:
|
|
||||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, keyctx)
|
|
||||||
if rv < 0:
|
|
||||||
raise ADEPTError('Failed to initialize AES key')
|
|
||||||
return self
|
|
||||||
def __init__(self):
|
|
||||||
self._blocksize = 0
|
|
||||||
self._keyctx = None
|
|
||||||
self._iv = 0
|
|
||||||
self._mode = 0
|
|
||||||
self._isDecrypt = None
|
|
||||||
def decrypt(self, data):
|
|
||||||
if not self._isDecrypt:
|
|
||||||
raise ADEPTError("AES not ready for decryption")
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise ADEPTError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
def encrypt(self, data):
|
|
||||||
if self._isDecrypt:
|
|
||||||
raise ADEPTError("AES not ready for encryption")
|
|
||||||
out = create_string_buffer(len(data))
|
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 1)
|
|
||||||
if rv == 0:
|
|
||||||
raise ADEPTError('AES decryption failed')
|
|
||||||
return out.raw
|
|
||||||
|
|
||||||
return (ARC4, RSA, AES)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.PublicKey import RSA as _RSA
|
|
||||||
from Crypto.Cipher import ARC4 as _ARC4
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
|
||||||
|
|
||||||
# ASN.1 parsing code from tlslite
|
|
||||||
class ASN1Error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ASN1Parser(object):
|
|
||||||
class Parser(object):
|
|
||||||
def __init__(self, bytes):
|
|
||||||
self.bytes = bytes
|
|
||||||
self.index = 0
|
|
||||||
|
|
||||||
def get(self, length):
|
|
||||||
if self.index + length > len(self.bytes):
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
x = 0
|
|
||||||
for count in range(length):
|
|
||||||
x <<= 8
|
|
||||||
x |= self.bytes[self.index]
|
|
||||||
self.index += 1
|
|
||||||
return x
|
|
||||||
|
|
||||||
def getFixBytes(self, lengthBytes):
|
|
||||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
|
||||||
self.index += lengthBytes
|
|
||||||
return bytes
|
|
||||||
|
|
||||||
def getVarBytes(self, lengthLength):
|
|
||||||
lengthBytes = self.get(lengthLength)
|
|
||||||
return self.getFixBytes(lengthBytes)
|
|
||||||
|
|
||||||
def getFixList(self, length, lengthList):
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def getVarList(self, length, lengthLength):
|
|
||||||
lengthList = self.get(lengthLength)
|
|
||||||
if lengthList % length != 0:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
lengthList = int(lengthList/length)
|
|
||||||
l = [0] * lengthList
|
|
||||||
for x in range(lengthList):
|
|
||||||
l[x] = self.get(length)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def startLengthCheck(self, lengthLength):
|
|
||||||
self.lengthCheck = self.get(lengthLength)
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def setLengthCheck(self, length):
|
|
||||||
self.lengthCheck = length
|
|
||||||
self.indexCheck = self.index
|
|
||||||
|
|
||||||
def stopLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def atLengthCheck(self):
|
|
||||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
|
||||||
return False
|
|
||||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise ASN1Error("Error decoding ASN.1")
|
|
||||||
|
|
||||||
def __init__(self, bytes):
|
|
||||||
p = self.Parser(bytes)
|
|
||||||
p.get(1)
|
|
||||||
self.length = self._getASN1Length(p)
|
|
||||||
self.value = p.getFixBytes(self.length)
|
|
||||||
|
|
||||||
def getChild(self, which):
|
|
||||||
p = self.Parser(self.value)
|
|
||||||
for x in range(which+1):
|
|
||||||
markIndex = p.index
|
|
||||||
p.get(1)
|
|
||||||
length = self._getASN1Length(p)
|
|
||||||
p.getFixBytes(length)
|
|
||||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
|
||||||
|
|
||||||
def _getASN1Length(self, p):
|
|
||||||
firstLength = p.get(1)
|
|
||||||
if firstLength<=127:
|
|
||||||
return firstLength
|
|
||||||
else:
|
|
||||||
lengthLength = firstLength & 0x7F
|
|
||||||
return p.get(lengthLength)
|
|
||||||
|
|
||||||
class ARC4(object):
|
|
||||||
@classmethod
|
|
||||||
def new(cls, userkey):
|
|
||||||
self = ARC4()
|
|
||||||
self._arc4 = _ARC4.new(userkey)
|
|
||||||
return self
|
|
||||||
def __init__(self):
|
|
||||||
self._arc4 = None
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._arc4.decrypt(data)
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
MODE_CBC = _AES.MODE_CBC
|
|
||||||
@classmethod
|
|
||||||
def new(cls, userkey, mode, iv, decrypt=True):
|
|
||||||
self = AES()
|
|
||||||
self._aes = _AES.new(userkey, mode, iv)
|
|
||||||
self._decrypt = decrypt
|
|
||||||
return self
|
|
||||||
def __init__(self):
|
|
||||||
self._aes = None
|
|
||||||
self._decrypt = None
|
|
||||||
def decrypt(self, data):
|
|
||||||
if not self._decrypt:
|
|
||||||
raise ADEPTError("AES not ready for decrypt.")
|
|
||||||
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
def encrypt(self, data):
|
|
||||||
if self._decrypt:
|
|
||||||
raise ADEPTError("AES not ready for encrypt.")
|
|
||||||
return self._aes.encrypt(data)
|
|
||||||
|
|
||||||
class RSA(object):
|
|
||||||
def __init__(self, der):
|
|
||||||
key = ASN1Parser([x for x in der])
|
|
||||||
key = [key.getChild(x).value for x in range(1, 4)]
|
|
||||||
key = [self.bytesToNumber(v) for v in key]
|
|
||||||
self._rsa = _RSA.construct(key)
|
|
||||||
|
|
||||||
def bytesToNumber(self, bytes):
|
|
||||||
total = 0
|
|
||||||
for byte in bytes:
|
|
||||||
total = (total << 8) + byte
|
|
||||||
return total
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
|
|
||||||
|
|
||||||
return (ARC4, RSA, AES)
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
ARC4 = RSA = AES = None
|
|
||||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
|
||||||
for loader in cryptolist:
|
|
||||||
try:
|
|
||||||
ARC4, RSA, AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, ADEPTError):
|
|
||||||
pass
|
|
||||||
return (ARC4, RSA, AES)
|
|
||||||
ARC4, RSA, AES = _load_crypto()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Do we generate cross reference streams on output?
|
# Do we generate cross reference streams on output?
|
||||||
# 0 = never
|
# 0 = never
|
||||||
# 1 = only if present in input
|
# 1 = only if present in input
|
||||||
# 2 = always
|
# 2 = always
|
||||||
|
|
||||||
GEN_XREF_STM = 0
|
GEN_XREF_STM = 1
|
||||||
|
|
||||||
# This is the value for the current document
|
# This is the value for the current document
|
||||||
gen_xref_stm = False # will be set in PDFSerializer
|
gen_xref_stm = False # will be set in PDFSerializer
|
||||||
@@ -482,7 +147,10 @@ def nunpack(s, default=0):
|
|||||||
elif l == 2:
|
elif l == 2:
|
||||||
return struct.unpack('>H', s)[0]
|
return struct.unpack('>H', s)[0]
|
||||||
elif l == 3:
|
elif l == 3:
|
||||||
return struct.unpack('>L', bytes([0]) + s)[0]
|
if sys.version_info[0] == 2:
|
||||||
|
return struct.unpack('>L', '\x00'+s)[0]
|
||||||
|
else:
|
||||||
|
return struct.unpack('>L', bytes([0]) + s)[0]
|
||||||
elif l == 4:
|
elif l == 4:
|
||||||
return struct.unpack('>L', s)[0]
|
return struct.unpack('>L', s)[0]
|
||||||
else:
|
else:
|
||||||
@@ -603,6 +271,11 @@ END_STRING = re.compile(br'[()\\]')
|
|||||||
OCT_STRING = re.compile(br'[0-7]')
|
OCT_STRING = re.compile(br'[0-7]')
|
||||||
ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
|
ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
|
||||||
|
|
||||||
|
class EmptyArrayValue(object):
|
||||||
|
def __str__(self):
|
||||||
|
return "<>"
|
||||||
|
|
||||||
|
|
||||||
class PSBaseParser(object):
|
class PSBaseParser(object):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -741,7 +414,10 @@ class PSBaseParser(object):
|
|||||||
self.hex += c
|
self.hex += c
|
||||||
return (self.parse_literal_hex, i+1)
|
return (self.parse_literal_hex, i+1)
|
||||||
if self.hex:
|
if self.hex:
|
||||||
self.token += bytes([int(self.hex, 16)])
|
if sys.version_info[0] == 2:
|
||||||
|
self.token += chr(int(self.hex, 16))
|
||||||
|
else:
|
||||||
|
self.token += bytes([int(self.hex, 16)])
|
||||||
return (self.parse_literal, i)
|
return (self.parse_literal, i)
|
||||||
|
|
||||||
def parse_number(self, s, i):
|
def parse_number(self, s, i):
|
||||||
@@ -825,10 +501,18 @@ class PSBaseParser(object):
|
|||||||
self.oct += c
|
self.oct += c
|
||||||
return (self.parse_string_1, i+1)
|
return (self.parse_string_1, i+1)
|
||||||
if self.oct:
|
if self.oct:
|
||||||
self.token += bytes([int(self.oct, 8)])
|
if sys.version_info[0] == 2:
|
||||||
|
self.token += chr(int(self.oct, 8))
|
||||||
|
else:
|
||||||
|
self.token += bytes([int(self.oct, 8)])
|
||||||
return (self.parse_string, i)
|
return (self.parse_string, i)
|
||||||
if c in ESC_STRING:
|
if c in ESC_STRING:
|
||||||
self.token += bytes([ESC_STRING[c]])
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
self.token += chr(ESC_STRING[c])
|
||||||
|
else:
|
||||||
|
self.token += bytes([ESC_STRING[c]])
|
||||||
|
|
||||||
return (self.parse_string, i+1)
|
return (self.parse_string, i+1)
|
||||||
|
|
||||||
def parse_wopen(self, s, i):
|
def parse_wopen(self, s, i):
|
||||||
@@ -841,6 +525,13 @@ class PSBaseParser(object):
|
|||||||
if c == b'<':
|
if c == b'<':
|
||||||
self.add_token(KEYWORD_DICT_BEGIN)
|
self.add_token(KEYWORD_DICT_BEGIN)
|
||||||
i += 1
|
i += 1
|
||||||
|
if c == b'>':
|
||||||
|
# Empty array without any contents. Why though?
|
||||||
|
# We need to add some dummy python object that will serialize to
|
||||||
|
# nothing, otherwise the code removes the whole array.
|
||||||
|
self.add_token(EmptyArrayValue())
|
||||||
|
i += 1
|
||||||
|
|
||||||
return (self.parse_main, i)
|
return (self.parse_main, i)
|
||||||
|
|
||||||
def parse_wclose(self, s, i):
|
def parse_wclose(self, s, i):
|
||||||
@@ -854,13 +545,17 @@ class PSBaseParser(object):
|
|||||||
return (self.parse_main, i)
|
return (self.parse_main, i)
|
||||||
|
|
||||||
def parse_hexstring(self, s, i):
|
def parse_hexstring(self, s, i):
|
||||||
m1 = END_HEX_STRING.search(s, i)
|
m = END_HEX_STRING.search(s, i)
|
||||||
if not m1:
|
if not m:
|
||||||
self.token += s[i:]
|
self.token += s[i:]
|
||||||
return (self.parse_hexstring, len(s))
|
return (self.parse_hexstring, len(s))
|
||||||
j = m1.start(0)
|
j = m.start(0)
|
||||||
self.token += s[i:j]
|
self.token += s[i:j]
|
||||||
token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]),
|
if sys.version_info[0] == 2:
|
||||||
|
token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)),
|
||||||
|
SPC.sub('', self.token))
|
||||||
|
else:
|
||||||
|
token = HEX_PAIR.sub(lambda m: bytes([int(m.group(0), 16)]),
|
||||||
SPC.sub(b'', self.token))
|
SPC.sub(b'', self.token))
|
||||||
self.add_token(token)
|
self.add_token(token)
|
||||||
return (self.parse_main, j)
|
return (self.parse_main, j)
|
||||||
@@ -882,7 +577,11 @@ class PSBaseParser(object):
|
|||||||
while 1:
|
while 1:
|
||||||
self.fillbuf()
|
self.fillbuf()
|
||||||
if eol:
|
if eol:
|
||||||
c = bytes([self.buf[self.charpos]])
|
if sys.version_info[0] == 2:
|
||||||
|
c = self.buf[self.charpos]
|
||||||
|
else:
|
||||||
|
c = bytes([self.buf[self.charpos]])
|
||||||
|
|
||||||
# handle '\r\n'
|
# handle '\r\n'
|
||||||
if c == b'\n':
|
if c == b'\n':
|
||||||
linebuf += c
|
linebuf += c
|
||||||
@@ -892,10 +591,17 @@ class PSBaseParser(object):
|
|||||||
if m:
|
if m:
|
||||||
linebuf += self.buf[self.charpos:m.end(0)]
|
linebuf += self.buf[self.charpos:m.end(0)]
|
||||||
self.charpos = m.end(0)
|
self.charpos = m.end(0)
|
||||||
if bytes([linebuf[-1]]) == b'\r':
|
if sys.version_info[0] == 2:
|
||||||
eol = True
|
if linebuf[-1] == b'\r':
|
||||||
|
eol = True
|
||||||
|
else:
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
break
|
if bytes([linebuf[-1]]) == b'\r':
|
||||||
|
eol = True
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
linebuf += self.buf[self.charpos:]
|
linebuf += self.buf[self.charpos:]
|
||||||
self.charpos = len(self.buf)
|
self.charpos = len(self.buf)
|
||||||
@@ -1128,7 +834,7 @@ def num_value(x):
|
|||||||
x = resolve1(x)
|
x = resolve1(x)
|
||||||
if not (isinstance(x, int) or isinstance(x, Decimal)):
|
if not (isinstance(x, int) or isinstance(x, Decimal)):
|
||||||
if STRICT:
|
if STRICT:
|
||||||
raise PDFTypeError('Int or Float required: %r' % x)
|
raise PDFTypeError('Int or Decimal required: %r' % x)
|
||||||
return 0
|
return 0
|
||||||
return x
|
return x
|
||||||
|
|
||||||
@@ -1271,9 +977,14 @@ class PDFStream(PDFObject):
|
|||||||
for i in range(0, len(data), columns+1):
|
for i in range(0, len(data), columns+1):
|
||||||
pred = data[i]
|
pred = data[i]
|
||||||
ent1 = data[i+1:i+1+columns]
|
ent1 = data[i+1:i+1+columns]
|
||||||
if pred == 2:
|
if sys.version_info[0] == 2:
|
||||||
ent1 = b''.join(bytes([(a+b) & 255]) \
|
if pred == '\x02':
|
||||||
for (a,b) in zip(ent0,ent1))
|
ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \
|
||||||
|
for (a,b) in zip(ent0,ent1))
|
||||||
|
else:
|
||||||
|
if pred == 2:
|
||||||
|
ent1 = b''.join(bytes([(a+b) & 255]) \
|
||||||
|
for (a,b) in zip(ent0,ent1))
|
||||||
buf += ent1
|
buf += ent1
|
||||||
ent0 = ent1
|
ent0 = ent1
|
||||||
data = buf
|
data = buf
|
||||||
@@ -1654,16 +1365,16 @@ class PDFDocument(object):
|
|||||||
return file_key
|
return file_key
|
||||||
|
|
||||||
|
|
||||||
def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
|
def process_with_aes(self, key, encrypt, data, repetitions = 1, iv = None):
|
||||||
if iv is None:
|
if iv is None:
|
||||||
keylen = len(key)
|
iv = bytes(bytearray(16))
|
||||||
iv = bytes([0x00]*keylen)
|
|
||||||
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
|
||||||
if not encrypt:
|
if not encrypt:
|
||||||
plaintext = AES.new(key,AES.MODE_CBC,iv, True).decrypt(data)
|
plaintext = aes.decrypt(data)
|
||||||
return plaintext
|
return plaintext
|
||||||
else:
|
else:
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv, False)
|
|
||||||
new_data = bytes(data * repetitions)
|
new_data = bytes(data * repetitions)
|
||||||
crypt = aes.encrypt(new_data)
|
crypt = aes.encrypt(new_data)
|
||||||
return crypt
|
return crypt
|
||||||
@@ -1684,27 +1395,18 @@ class PDFDocument(object):
|
|||||||
raise Exception("K1 < 32 ...")
|
raise Exception("K1 < 32 ...")
|
||||||
#def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
|
#def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
|
||||||
E = self.process_with_aes(K[:16], True, K1, 64, K[16:32])
|
E = self.process_with_aes(K[:16], True, K1, 64, K[16:32])
|
||||||
|
E = bytearray(E)
|
||||||
|
|
||||||
E_mod_3 = 0
|
E_mod_3 = 0
|
||||||
for i in range(16):
|
for i in range(16):
|
||||||
E_mod_3 += E[i]
|
E_mod_3 += E[i]
|
||||||
E_mod_3 = E_mod_3 % 3
|
|
||||||
|
|
||||||
if E_mod_3 == 0:
|
E_mod_3 %= 3
|
||||||
ctx = hashlib.sha256()
|
|
||||||
ctx.update(E)
|
K = (hashlib.sha256, hashlib.sha384, hashlib.sha512)[E_mod_3](E).digest()
|
||||||
K = ctx.digest()
|
|
||||||
elif E_mod_3 == 1:
|
|
||||||
ctx = hashlib.sha384()
|
|
||||||
ctx.update(E)
|
|
||||||
K = ctx.digest()
|
|
||||||
else:
|
|
||||||
ctx = hashlib.sha512()
|
|
||||||
ctx.update(E)
|
|
||||||
K = ctx.digest()
|
|
||||||
|
|
||||||
if round_number >= 64:
|
if round_number >= 64:
|
||||||
ch = int.from_bytes(E[-1:], "big", signed=False)
|
ch = E[-1:][0] # get last byte
|
||||||
if ch <= round_number - 32:
|
if ch <= round_number - 32:
|
||||||
done = True
|
done = True
|
||||||
|
|
||||||
@@ -1738,7 +1440,10 @@ class PDFDocument(object):
|
|||||||
x = ARC4.new(hash).decrypt(Odata) # 4
|
x = ARC4.new(hash).decrypt(Odata) # 4
|
||||||
if R >= 3:
|
if R >= 3:
|
||||||
for i in range(1,19+1):
|
for i in range(1,19+1):
|
||||||
k = b''.join(bytes([c ^ i]) for c in hash )
|
if sys.version_info[0] == 2:
|
||||||
|
k = b''.join(chr(ord(c) ^ i) for c in hash )
|
||||||
|
else:
|
||||||
|
k = b''.join(bytes([c ^ i]) for c in hash )
|
||||||
x = ARC4.new(k).decrypt(x)
|
x = ARC4.new(k).decrypt(x)
|
||||||
|
|
||||||
|
|
||||||
@@ -1782,23 +1487,36 @@ class PDFDocument(object):
|
|||||||
EncMetadata = b'True'
|
EncMetadata = b'True'
|
||||||
if (EncMetadata == ('False' or 'false') or V < 4) and R >= 4:
|
if (EncMetadata == ('False' or 'false') or V < 4) and R >= 4:
|
||||||
hash.update(codecs.decode(b'ffffffff','hex'))
|
hash.update(codecs.decode(b'ffffffff','hex'))
|
||||||
|
|
||||||
|
# Finish hash:
|
||||||
|
hash = hash.digest()
|
||||||
|
|
||||||
if R >= 3:
|
if R >= 3:
|
||||||
# 8
|
# 8
|
||||||
for _ in range(50):
|
for _ in range(50):
|
||||||
hash = hashlib.md5(hash.digest()[:length//8])
|
hash = hashlib.md5(hash[:length//8]).digest()
|
||||||
key = hash.digest()[:length//8]
|
if R == 2:
|
||||||
|
# R=2 only uses first five bytes.
|
||||||
|
key = hash[:5]
|
||||||
|
else:
|
||||||
|
key = hash[:length//8]
|
||||||
|
|
||||||
if R == 2:
|
if R == 2:
|
||||||
# Algorithm 3.4
|
# Algorithm 3.4
|
||||||
u1 = ARC4.new(key).decrypt(password)
|
u1 = ARC4.new(key).decrypt(self.PASSWORD_PADDING)
|
||||||
elif R >= 3:
|
elif R >= 3:
|
||||||
# Algorithm 3.5
|
# Algorithm 3.5
|
||||||
hash = hashlib.md5(self.PASSWORD_PADDING) # 2
|
hash = hashlib.md5(self.PASSWORD_PADDING) # 2
|
||||||
hash.update(docid[0]) # 3
|
hash.update(docid[0]) # 3
|
||||||
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
|
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
|
||||||
for i in range(1,19+1):
|
for i in range(1,19+1):
|
||||||
k = b''.join(bytes([c ^ i]) for c in key )
|
if sys.version_info[0] == 2:
|
||||||
|
k = b''.join(chr(ord(c) ^ i) for c in key )
|
||||||
|
else:
|
||||||
|
k = b''.join(bytes([c ^ i]) for c in key )
|
||||||
x = ARC4.new(k).decrypt(x)
|
x = ARC4.new(k).decrypt(x)
|
||||||
u1 = x+x # 32bytes total
|
u1 = x+x # 32bytes total
|
||||||
|
|
||||||
if R == 2:
|
if R == 2:
|
||||||
is_authenticated = (u1 == U)
|
is_authenticated = (u1 == U)
|
||||||
else:
|
else:
|
||||||
@@ -1824,8 +1542,8 @@ class PDFDocument(object):
|
|||||||
|
|
||||||
# check owner pass:
|
# check owner pass:
|
||||||
retval = self.check_owner_password(password, docid, param)
|
retval = self.check_owner_password(password, docid, param)
|
||||||
if retval is True or retval is not None:
|
if retval is True or (retval is not False and retval is not None):
|
||||||
#print("Owner pass is valid - " + str(retval))
|
#print("Owner pass is valid")
|
||||||
if retval is True:
|
if retval is True:
|
||||||
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
||||||
else:
|
else:
|
||||||
@@ -1834,7 +1552,7 @@ class PDFDocument(object):
|
|||||||
if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
|
if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
|
||||||
# That's not the owner password. Check if it's the user password.
|
# That's not the owner password. Check if it's the user password.
|
||||||
retval = self.check_user_password(password, docid, param)
|
retval = self.check_user_password(password, docid, param)
|
||||||
if retval is True or retval is not None:
|
if retval is True or (retval is not False and retval is not None):
|
||||||
#print("User pass is valid")
|
#print("User pass is valid")
|
||||||
if retval is True:
|
if retval is True:
|
||||||
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
||||||
@@ -1900,39 +1618,28 @@ class PDFDocument(object):
|
|||||||
self.ready = True
|
self.ready = True
|
||||||
return
|
return
|
||||||
|
|
||||||
def verify_book_key(self, bookkey):
|
|
||||||
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
|
|
||||||
# Byte not null, invalid result
|
|
||||||
return False
|
|
||||||
|
|
||||||
if ((bookkey[0] != '\x02' and bookkey[0] != 2) and
|
|
||||||
((bookkey[0] != '\x00' and bookkey[0] != 0) or
|
|
||||||
(bookkey[1] != '\x02' and bookkey[1] != 2))):
|
|
||||||
# Key not starting with "00 02" or "02" -> error
|
|
||||||
return False
|
|
||||||
|
|
||||||
keylen = len(bookkey) - 17
|
|
||||||
for i in range(1, keylen):
|
|
||||||
if bookkey[i] == 0 or bookkey[i] == '\x00':
|
|
||||||
# Padding data contains a space - that's not allowed.
|
|
||||||
# Probably bad decryption.
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def initialize_ebx_ignoble(self, keyb64, docid, param):
|
def initialize_ebx_ignoble(self, keyb64, docid, param):
|
||||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||||
key = keyb64.decode('base64')[:16]
|
|
||||||
aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key))
|
try:
|
||||||
|
key = keyb64.decode('base64')[:16]
|
||||||
|
# This will probably always error, but I'm not 100% sure, so lets leave the old code in.
|
||||||
|
except AttributeError:
|
||||||
|
key = codecs.decode(keyb64.encode("ascii"), 'base64')[:16]
|
||||||
|
|
||||||
|
|
||||||
length = int_value(param.get('Length', 0)) / 8
|
length = int_value(param.get('Length', 0)) / 8
|
||||||
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
|
rights = codecs.decode(str_value(param.get('ADEPT_LICENSE')), "base64")
|
||||||
rights = zlib.decompress(rights, -15)
|
rights = zlib.decompress(rights, -15)
|
||||||
rights = etree.fromstring(rights)
|
rights = etree.fromstring(rights)
|
||||||
expr = './/{http://ns.adobe.com/adept}encryptedKey'
|
expr = './/{http://ns.adobe.com/adept}encryptedKey'
|
||||||
bookkey = ''.join(rights.findtext(expr)).decode('base64')
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
bookkey = aes.decrypt(bookkey)
|
bookkey = base64.b64decode(bookkey)
|
||||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
bookkey = AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey)
|
||||||
bookkey = bookkey[-16:]
|
bookkey = unpad(bookkey, 16) # PKCS#7
|
||||||
|
if len(bookkey) > 16:
|
||||||
|
bookkey = bookkey[-16:]
|
||||||
ebx_V = int_value(param.get('V', 4))
|
ebx_V = int_value(param.get('V', 4))
|
||||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||||
# added because of improper booktype / decryption book session key errors
|
# added because of improper booktype / decryption book session key errors
|
||||||
@@ -1948,13 +1655,15 @@ class PDFDocument(object):
|
|||||||
else:
|
else:
|
||||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||||
print("bookkey[0] is %d" % bookkey[0])
|
if len(bookkey) > 0:
|
||||||
|
print("bookkey[0] is %d" % bookkey[0])
|
||||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||||
else:
|
else:
|
||||||
# proper length unknown try with whatever you have
|
# proper length unknown try with whatever you have
|
||||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||||
print("bookkey[0] is %d" % ord(bookkey[0]))
|
if len(bookkey) > 0:
|
||||||
|
print("bookkey[0] is %d" % ord(bookkey[0]))
|
||||||
if ebx_V == 3:
|
if ebx_V == 3:
|
||||||
V = 3
|
V = 3
|
||||||
else:
|
else:
|
||||||
@@ -1965,32 +1674,45 @@ class PDFDocument(object):
|
|||||||
self.ready = True
|
self.ready = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def removeHardening(rights, keytype, keydata):
|
||||||
|
adept = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
|
||||||
|
|
||||||
|
# Gather what we need, and generate the IV
|
||||||
|
resourceuuid = UUID(textGetter("resource"))
|
||||||
|
deviceuuid = UUID(textGetter("device"))
|
||||||
|
fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
|
||||||
|
kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
|
||||||
|
|
||||||
|
# Derive kek from just "keytype"
|
||||||
|
rem = int(keytype, 10) % 16
|
||||||
|
H = SHA256(keytype.encode("ascii"))
|
||||||
|
kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
|
||||||
|
|
||||||
|
return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16)
|
||||||
|
|
||||||
def initialize_ebx_inept(self, password, docid, param):
|
def initialize_ebx_inept(self, password, docid, param):
|
||||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||||
rsa = RSA(password)
|
rsakey = RSA.importKey(password) # parses the ASN1 structure
|
||||||
length = int_value(param.get('Length', 0)) // 8
|
length = int_value(param.get('Length', 0)) // 8
|
||||||
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
|
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
|
||||||
rights = zlib.decompress(rights, -15)
|
rights = zlib.decompress(rights, -15)
|
||||||
rights = etree.fromstring(rights)
|
rights = etree.fromstring(rights)
|
||||||
expr = './/{http://ns.adobe.com/adept}encryptedKey'
|
expr = './/{http://ns.adobe.com/adept}encryptedKey'
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
bookkeyelem = rights.find(expr)
|
||||||
|
bookkey = codecs.decode(bookkeyelem.text.encode('utf-8'),'base64')
|
||||||
|
keytype = bookkeyelem.attrib.get('keyType', '0')
|
||||||
|
|
||||||
if len(bookkey) == 192:
|
if int(keytype, 10) > 2:
|
||||||
print("This seems to be an Adobe ADEPT PDF with Adobe's new DRM")
|
bookkey = PDFDocument.removeHardening(rights, keytype, bookkey)
|
||||||
print("This DRM cannot be removed yet. ")
|
try:
|
||||||
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
|
bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
|
||||||
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
|
except ValueError:
|
||||||
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
|
bookkey = None
|
||||||
|
|
||||||
bookkey = codecs.decode(bookkey.encode('utf-8'),'base64')
|
if bookkey is None:
|
||||||
bookkey = rsa.decrypt(bookkey)
|
raise ADEPTError('error decrypting book session key')
|
||||||
|
|
||||||
if len(bookkey) > 16:
|
|
||||||
if (self.verify_book_key(bookkey)):
|
|
||||||
bookkey = bookkey[-16:]
|
|
||||||
length = 16
|
|
||||||
else:
|
|
||||||
raise ADEPTError('error decrypting book session key')
|
|
||||||
|
|
||||||
ebx_V = int_value(param.get('V', 4))
|
ebx_V = int_value(param.get('V', 4))
|
||||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||||
@@ -2007,13 +1729,15 @@ class PDFDocument(object):
|
|||||||
else:
|
else:
|
||||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||||
print("bookkey[0] is %d" % bookkey[0])
|
if len(bookkey) > 0:
|
||||||
|
print("bookkey[0] is %d" % bookkey[0])
|
||||||
raise ADEPTError('error decrypting book session key - mismatched length')
|
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||||
else:
|
else:
|
||||||
# proper length unknown try with whatever you have
|
# proper length unknown try with whatever you have
|
||||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||||
print("bookkey[0] is %d" % bookkey[0])
|
if len(bookkey) > 0:
|
||||||
|
print("bookkey[0] is %d" % bookkey[0])
|
||||||
if ebx_V == 3:
|
if ebx_V == 3:
|
||||||
V = 3
|
V = 3
|
||||||
else:
|
else:
|
||||||
@@ -2037,7 +1761,7 @@ class PDFDocument(object):
|
|||||||
objid = struct.pack('<L', objid ^ 0x3569ac)
|
objid = struct.pack('<L', objid ^ 0x3569ac)
|
||||||
genno = struct.pack('<L', genno ^ 0xca96)
|
genno = struct.pack('<L', genno ^ 0xca96)
|
||||||
key = self.decrypt_key
|
key = self.decrypt_key
|
||||||
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + b'sAlT'
|
key += bytes([objid[0], genno[0], objid[1], genno[1], objid[2]]) + b'sAlT'
|
||||||
hash = hashlib.md5(key)
|
hash = hashlib.md5(key)
|
||||||
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
|
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
|
||||||
return key
|
return key
|
||||||
@@ -2061,7 +1785,11 @@ class PDFDocument(object):
|
|||||||
data = data[16:]
|
data = data[16:]
|
||||||
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
|
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
|
||||||
# remove pkcs#5 aes padding
|
# remove pkcs#5 aes padding
|
||||||
cutter = -1 * plaintext[-1]
|
if sys.version_info[0] == 2:
|
||||||
|
cutter = -1 * ord(plaintext[-1])
|
||||||
|
else:
|
||||||
|
cutter = -1 * plaintext[-1]
|
||||||
|
|
||||||
plaintext = plaintext[:cutter]
|
plaintext = plaintext[:cutter]
|
||||||
return plaintext
|
return plaintext
|
||||||
|
|
||||||
@@ -2122,7 +1850,19 @@ class PDFDocument(object):
|
|||||||
try:
|
try:
|
||||||
obj = objs[i]
|
obj = objs[i]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise PDFSyntaxError('Invalid object number: objid=%r' % (objid))
|
# This IndexError used to just raise an exception.
|
||||||
|
# Unfortunately that seems to break some PDFs, see this issue:
|
||||||
|
# https://github.com/noDRM/DeDRM_tools/issues/233
|
||||||
|
# I'm not sure why this is the case, but lets try only raising that exception
|
||||||
|
# when in STRICT mode, and make it a warning otherwise.
|
||||||
|
if STRICT:
|
||||||
|
raise PDFSyntaxError('Invalid object number: objid=%r' % (objid))
|
||||||
|
|
||||||
|
print('Invalid object number: objid=%r' % (objid))
|
||||||
|
print("Continuing anyways?")
|
||||||
|
print("If the resulting PDF is corrupted, please open a bug report.")
|
||||||
|
return None
|
||||||
|
|
||||||
if isinstance(obj, PDFStream):
|
if isinstance(obj, PDFStream):
|
||||||
obj.set_objid(objid, 0)
|
obj.set_objid(objid, 0)
|
||||||
else:
|
else:
|
||||||
@@ -2302,7 +2042,7 @@ class PDFParser(PSStackParser):
|
|||||||
except PDFNoValidXRef:
|
except PDFNoValidXRef:
|
||||||
# fallback
|
# fallback
|
||||||
self.seek(0)
|
self.seek(0)
|
||||||
pat = re.compile(b'^(\\d+)\\s+(\\d+)\\s+obj\\b')
|
pat = re.compile(br'^(\\d+)\\s+(\\d+)\\s+obj\\b')
|
||||||
offsets = {}
|
offsets = {}
|
||||||
xref = PDFXRef()
|
xref = PDFXRef()
|
||||||
while 1:
|
while 1:
|
||||||
@@ -2537,7 +2277,7 @@ class PDFSerializer(object):
|
|||||||
elif isinstance(obj, bytearray):
|
elif isinstance(obj, bytearray):
|
||||||
self.write(b'(%s)' % self.escape_string(obj))
|
self.write(b'(%s)' % self.escape_string(obj))
|
||||||
elif isinstance(obj, bytes):
|
elif isinstance(obj, bytes):
|
||||||
self.write(b'(%s)' % self.escape_string(obj))
|
self.write(b'<%s>' % binascii.hexlify(obj).upper())
|
||||||
elif isinstance(obj, str):
|
elif isinstance(obj, str):
|
||||||
self.write(b'(%s)' % self.escape_string(obj.encode('utf-8')))
|
self.write(b'(%s)' % self.escape_string(obj.encode('utf-8')))
|
||||||
elif isinstance(obj, bool):
|
elif isinstance(obj, bool):
|
||||||
@@ -2564,6 +2304,20 @@ class PDFSerializer(object):
|
|||||||
self.write(b'(deleted)')
|
self.write(b'(deleted)')
|
||||||
else:
|
else:
|
||||||
data = obj.get_decdata()
|
data = obj.get_decdata()
|
||||||
|
|
||||||
|
# Fix length:
|
||||||
|
# We've decompressed and then recompressed the PDF stream.
|
||||||
|
# Depending on the algorithm, the implementation, and the compression level,
|
||||||
|
# the resulting recompressed stream is unlikely to have the same length as the original.
|
||||||
|
# So we need to update the PDF object to contain the new proper length.
|
||||||
|
|
||||||
|
# Without this change, all PDFs exported by this plugin are slightly corrupted -
|
||||||
|
# even though most if not all PDF readers can correct that on-the-fly.
|
||||||
|
|
||||||
|
if 'Length' in obj.dic:
|
||||||
|
obj.dic['Length'] = len(data)
|
||||||
|
|
||||||
|
|
||||||
self.serialize_object(obj.dic)
|
self.serialize_object(obj.dic)
|
||||||
self.write(b'stream\n')
|
self.write(b'stream\n')
|
||||||
self.write(data)
|
self.write(data)
|
||||||
@@ -2585,8 +2339,6 @@ class PDFSerializer(object):
|
|||||||
|
|
||||||
|
|
||||||
def decryptBook(userkey, inpath, outpath, inept=True):
|
def decryptBook(userkey, inpath, outpath, inept=True):
|
||||||
if RSA is None:
|
|
||||||
raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
|
|
||||||
with open(inpath, 'rb') as inf:
|
with open(inpath, 'rb') as inf:
|
||||||
serializer = PDFSerializer(inf, userkey, inept)
|
serializer = PDFSerializer(inf, userkey, inept)
|
||||||
with open(outpath, 'wb') as outf:
|
with open(outpath, 'wb') as outf:
|
||||||
@@ -2601,8 +2353,6 @@ def decryptBook(userkey, inpath, outpath, inept=True):
|
|||||||
|
|
||||||
|
|
||||||
def getPDFencryptionType(inpath):
|
def getPDFencryptionType(inpath):
|
||||||
if RSA is None:
|
|
||||||
raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
|
|
||||||
with open(inpath, 'rb') as inf:
|
with open(inpath, 'rb') as inf:
|
||||||
doc = doc = PDFDocument()
|
doc = doc = PDFDocument()
|
||||||
parser = PDFParser(doc, inf)
|
parser = PDFParser(doc, inf)
|
||||||
@@ -2614,7 +2364,7 @@ def getPDFencryptionType(inpath):
|
|||||||
def cli_main():
|
def cli_main():
|
||||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("ineptpdf.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
if len(argv) != 4:
|
if len(argv) != 4:
|
||||||
print("usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname))
|
print("usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname))
|
||||||
@@ -2735,13 +2485,6 @@ def gui_main():
|
|||||||
|
|
||||||
|
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
if RSA is None:
|
|
||||||
root.withdraw()
|
|
||||||
tkinter.messagebox.showerror(
|
|
||||||
"INEPT PDF",
|
|
||||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
|
||||||
"separately. Read the top-of-script comment for details.")
|
|
||||||
return 1
|
|
||||||
root.title("Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
root.title("Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
||||||
root.resizable(True, False)
|
root.resizable(True, False)
|
||||||
root.minsize(370, 0)
|
root.minsize(370, 0)
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ion.py
|
"""ion.py: Decrypt Kindle KFX files.
|
||||||
# Copyright © 2013-2020 Apprentice Harper et al.
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
Revision history:
|
||||||
__version__ = '3.0'
|
Pascal implementation by lulzkabulz.
|
||||||
|
BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||||
|
1.0 - Python translation by apprenticenaomi.
|
||||||
|
1.1 - DeDRM integration by anon.
|
||||||
|
1.2 - Added pylzma import fallback
|
||||||
|
1.3 - Fixed lzma support for calibre 4.6+
|
||||||
|
2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya.
|
||||||
|
3.0 - Added Python 3 compatibility for calibre 5.0
|
||||||
|
|
||||||
# Revision history:
|
Copyright © 2013-2020 Apprentice Harper et al.
|
||||||
# Pascal implementation by lulzkabulz.
|
|
||||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
|
||||||
# 1.0 - Python translation by apprenticenaomi.
|
|
||||||
# 1.1 - DeDRM integration by anon.
|
|
||||||
# 1.2 - Added pylzma import fallback
|
|
||||||
# 1.3 - Fixed lzma support for calibre 4.6+
|
|
||||||
# 2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya.
|
|
||||||
# 3.0 - Added Python 3 compatibility for calibre 5.0
|
|
||||||
|
|
||||||
"""
|
|
||||||
Decrypt Kindle KFX files.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
@@ -30,8 +25,18 @@ import struct
|
|||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
__license__ = 'GPL v3'
|
||||||
from Crypto.Util.py3compat import bchr
|
__version__ = '3.0'
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
from Cryptodome.Util.py3compat import bchr
|
||||||
|
except ImportError:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.py3compat import bchr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# lzma library from calibre 4.6.0 or later
|
# lzma library from calibre 4.6.0 or later
|
||||||
@@ -53,6 +58,7 @@ except ImportError:
|
|||||||
# Windows-friendly choice: pylzma wheels
|
# Windows-friendly choice: pylzma wheels
|
||||||
import pylzma as lzma
|
import pylzma as lzma
|
||||||
|
|
||||||
|
from .kfxtables import *
|
||||||
|
|
||||||
TID_NULL = 0
|
TID_NULL = 0
|
||||||
TID_BOOLEAN = 1
|
TID_BOOLEAN = 1
|
||||||
@@ -761,7 +767,11 @@ def pkcs7unpad(msg, blocklen):
|
|||||||
return msg[:-paddinglen]
|
return msg[:-paddinglen]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
|
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
|
||||||
|
# 4-digit versions use their own obfuscation/scramble. It does not seem to depend on the "word" and number
|
||||||
OBFUSCATION_TABLE = {
|
OBFUSCATION_TABLE = {
|
||||||
"V1": (0x00, None),
|
"V1": (0x00, None),
|
||||||
"V2": (0x05, b'Antidisestablishmentarianism'),
|
"V2": (0x05, b'Antidisestablishmentarianism'),
|
||||||
@@ -772,26 +782,26 @@ OBFUSCATION_TABLE = {
|
|||||||
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||||
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||||
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||||
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v;\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||||
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
|
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
|
||||||
"V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
|
"V12": (0x06, b';n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
|
||||||
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
|
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
|
||||||
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
|
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
|
||||||
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
|
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
|
||||||
"V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
|
"V16": (0x0A, b'\x06\x18`h;b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
|
||||||
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
|
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
|
||||||
"V18": (0x07, b'I\x0b\x0e,\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
|
"V18": (0x07, b'I\x0b\x0e;\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
|
||||||
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
|
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
|
||||||
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e,~hZ\x12jK\x17\x1e*1'),
|
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e;~hZ\x12jK\x17\x1e*1'),
|
||||||
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
|
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
|
||||||
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
|
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
|
||||||
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
|
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
|
||||||
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t,\rr`uv\\~55\x11]N'),
|
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t;\rr`uv\\~55\x11]N'),
|
||||||
"V25": (0x09, b'\x07\x14w\x1e,^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
|
"V25": (0x09, b'\x07\x14w\x1e;^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
|
||||||
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
|
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
|
||||||
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
|
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
|
||||||
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
|
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
|
||||||
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\}x04\x11\t\x06\x04?'),
|
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\x04\x11\t\x06\x04?'),
|
||||||
"V1031": (0x08, b'Antidisestablishmentarianism'),
|
"V1031": (0x08, b'Antidisestablishmentarianism'),
|
||||||
"V2069": (0x07, b'Floccinaucinihilipilification'),
|
"V2069": (0x07, b'Floccinaucinihilipilification'),
|
||||||
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
||||||
@@ -800,10 +810,367 @@ OBFUSCATION_TABLE = {
|
|||||||
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||||
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||||
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||||
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v;\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#common str: "PIDv3AESAES/CBC/PKCS5PaddingHmacSHA256"
|
||||||
|
class workspace(object):
|
||||||
|
def __init__(self,initial_list):
|
||||||
|
self.work=initial_list
|
||||||
|
def shuffle(self,shuflist):
|
||||||
|
ll=len(shuflist)
|
||||||
|
rt=[]
|
||||||
|
for i in range(ll):
|
||||||
|
rt.append(self.work[shuflist[i]])
|
||||||
|
self.work=rt
|
||||||
|
def sbox(self,table,matrix,skplist=[]): #table is list of 4-byte integers
|
||||||
|
offset=0
|
||||||
|
nwork=list(self.work)
|
||||||
|
wo=0
|
||||||
|
toff=0
|
||||||
|
while offset<0x6000:
|
||||||
|
uv5=table[toff+nwork[wo+0]]
|
||||||
|
uv1=table[toff+nwork[wo+1]+0x100]
|
||||||
|
uv2=table[toff+nwork[wo+2]+0x200]
|
||||||
|
uv3=table[toff+nwork[wo+3]+0x300]
|
||||||
|
moff=0
|
||||||
|
if 0 in skplist:
|
||||||
|
moff+=0x400
|
||||||
|
else:
|
||||||
|
nib1=matrix[moff+offset+(uv1>>0x1c)|( (uv5>>0x18)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib2=matrix[moff+offset+(uv3>>0x1c)|( (uv2>>0x18)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib3=matrix[moff+offset+((uv1>>0x18)&0xf) |( (uv5>>0x14)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib4=matrix[moff+offset+((uv3>>0x18)&0xf) |( (uv2>>0x14)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
rnib1=matrix[moff+offset+nib1*0x10+nib2]
|
||||||
|
moff+=0x100
|
||||||
|
rnib2=matrix[moff+offset+nib3*0x10+nib4]
|
||||||
|
moff+=0x100
|
||||||
|
nwork[wo+0]=rnib1*0x10+rnib2
|
||||||
|
if 1 in skplist:
|
||||||
|
moff+=0x400
|
||||||
|
else:
|
||||||
|
nib1=matrix[moff+offset+((uv1>>0x14)&0xf)|( (uv5>>0x10)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib2=matrix[moff+offset+((uv3>>0x14)&0xf)|( (uv2>>0x10)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib3=matrix[moff+offset+((uv1>>0x10)&0xf) |( (uv5>>0xc)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib4=matrix[moff+offset+((uv3>>0x10)&0xf) |( (uv2>>0xc)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
|
||||||
|
rnib1=matrix[moff+offset+nib1*0x10+nib2]
|
||||||
|
moff+=0x100
|
||||||
|
rnib2=matrix[moff+offset+nib3*0x10+nib4]
|
||||||
|
moff+=0x100
|
||||||
|
nwork[wo+1]=rnib1*0x10+rnib2
|
||||||
|
if 2 in skplist:
|
||||||
|
moff+=0x400
|
||||||
|
else:
|
||||||
|
nib1=matrix[moff+offset+((uv1>>0xc)&0xf)|( (uv5>>0x8)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib2=matrix[moff+offset+((uv3>>0xc)&0xf)|( (uv2>>0x8)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib3=matrix[moff+offset+((uv1>>0x8)&0xf) |( (uv5>>0x4)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib4=matrix[moff+offset+((uv3>>0x8)&0xf) |( (uv2>>0x4)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
rnib1=matrix[moff+offset+nib1*0x10+nib2]
|
||||||
|
moff+=0x100
|
||||||
|
rnib2=matrix[moff+offset+nib3*0x10+nib4]
|
||||||
|
moff+=0x100
|
||||||
|
nwork[wo+2]=rnib1*0x10+rnib2
|
||||||
|
if 3 in skplist:
|
||||||
|
moff+=0x400
|
||||||
|
else:
|
||||||
|
nib1=matrix[moff+offset+((uv1>>0x4)&0xf)|( (uv5)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib2=matrix[moff+offset+((uv3>>0x4)&0xf)|( (uv2)&0xf0)]
|
||||||
|
moff+=0x100
|
||||||
|
nib3=matrix[moff+offset+((uv1)&0xf)|( (uv5<<4)&0xf0) ]
|
||||||
|
moff+=0x100
|
||||||
|
nib4=matrix[moff+offset+((uv3)&0xf)|( (uv2<<4)&0xf0) ]
|
||||||
|
moff+=0x100
|
||||||
|
##############
|
||||||
|
rnib1=matrix[moff+offset+nib1*0x10+nib2]
|
||||||
|
moff+=0x100
|
||||||
|
rnib2=matrix[moff+offset+nib3*0x10+nib4]
|
||||||
|
moff+=0x100
|
||||||
|
nwork[wo+3]=rnib1*0x10+rnib2
|
||||||
|
offset = offset + 0x1800
|
||||||
|
wo+=4
|
||||||
|
toff+=0x400
|
||||||
|
self.work=nwork
|
||||||
|
def lookup(self,ltable):
|
||||||
|
for a in range(len(self.work)):
|
||||||
|
self.work[a]=ltable[a]
|
||||||
|
def exlookup(self,ltable):
|
||||||
|
lookoffs=0
|
||||||
|
for a in range(len(self.work)):
|
||||||
|
self.work[a]=ltable[self.work[a]+lookoffs]
|
||||||
|
lookoffs+=0x100
|
||||||
|
def mask(self, chunk):
|
||||||
|
out=[]
|
||||||
|
for a in range(len(chunk)):
|
||||||
|
self.work[a]=self.work[a]^chunk[a]
|
||||||
|
out.append(self.work[a])
|
||||||
|
return out
|
||||||
|
|
||||||
|
def process_V9708(st):
|
||||||
|
#e9c457a7dae6aa24365e7ef219b934b17ed58ee7d5329343fc3aea7860ed51f9a73de14351c9
|
||||||
|
ws=workspace([0x11]*16)
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a06ea70,d0x6a0dab50)
|
||||||
|
ws.sbox(d0x6a073a70,d0x6a0dab50)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.exlookup(d0x6a072a70)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16;
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V1031(st):
|
||||||
|
#d53efea7fdd0fda3e1e0ebbae87cad0e8f5ef413c471c3ae81f39222a9ec8b8ed582e045918c
|
||||||
|
ws=workspace([0x06,0x18,0x60,0x68,0x3b,0x62,0x3e,0x3c,0x06,0x50,0x71,0x52,0x02,0x5a,0x63,0x03])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0797c0,d0x6a0dab50,[3])
|
||||||
|
ws.sbox(d0x6a07e7c0,d0x6a0dab50,[3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0797c0,d0x6a0dab50,[3])
|
||||||
|
ws.sbox(d0x6a07e7c0,d0x6a0dab50,[3])
|
||||||
|
ws.exlookup(d0x6a07d7c0)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
#break
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V2069(st):
|
||||||
|
#8e6196d754a304c9354e91b5d79f07b048026d31c7373a8691e513f2c802c706742731caa858
|
||||||
|
ws=workspace([0x79,0x0d,0x12,0x08,0x66,0x77,0x2e,0x5b,0x02,0x09,0x0a,0x13,0x11,0x0c,0x11,0x62])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a084498,d0x6a0dab50,[2])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a089498,d0x6a0dab50,[2])
|
||||||
|
ws.sbox(d0x6a089498,d0x6a0dab50,[2])
|
||||||
|
ws.sbox(d0x6a084498,d0x6a0dab50,[2])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.exlookup(d0x6a088498)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def process_V9041(st):
|
||||||
|
#11f7db074b24e560dfa6fae3252b383c3b936e51f6ded570dc936cb1da9f4fc4a97ec686e7d8
|
||||||
|
ws=workspace([0x49,0x0b,0x0e,0x3b,0x19,0x1a,0x49,0x61,0x10,0x73,0x19,0x67,0x5c,0x1b,0x11,0x21])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a094170,d0x6a0dab50,[1])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a08f170,d0x6a0dab50,[1])
|
||||||
|
ws.sbox(d0x6a08f170,d0x6a0dab50,[1])
|
||||||
|
ws.sbox(d0x6a094170,d0x6a0dab50,[1])
|
||||||
|
|
||||||
|
ws.exlookup(d0x6a093170)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
#break
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V3646(st):
|
||||||
|
#d468aa362b44479282291983243b38197c4b4aa24c2c58e62c76ec4b81e08556ca0c54301664
|
||||||
|
ws=workspace([0x0a,0x36,0x3e,0x29,0x4e,0x02,0x18,0x38,0x01,0x36,0x73,0x13,0x14,0x1b,0x16,0x6a])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
|
||||||
|
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
|
||||||
|
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
|
||||||
|
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
|
||||||
|
ws.exlookup(d0x6a09de48)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def process_V6052(st):
|
||||||
|
#d683c8c4e4f46ae45812196f37e218eabce0fae08994f25fabb01d3e569b8bf3866b99d36f57
|
||||||
|
ws=workspace([0x5f,0x0d,0x01,0x12,0x5d,0x5c,0x14,0x2a,0x17,0x69,0x14,0x0d,0x09,0x21,0x1e,0x3b])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
|
||||||
|
|
||||||
|
ws.exlookup(d0x6a0a8b20)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V9479(st):
|
||||||
|
#925635db434bccd3f4791eb87b89d2dfc7c93be06e794744eb9de58e6d721e696980680ab551
|
||||||
|
ws=workspace([0x65,0x1d,0x19,0x7c,0x09,0x79,0x1d,0x69,0x7c,0x4e,0x13,0x0e,0x04,0x1b,0x6a,0x3c ])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
|
||||||
|
ws.exlookup(d0x6a0b37f8)
|
||||||
|
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V9888(st):
|
||||||
|
#54c470723f8c105ba0186b6319050869de673ce31a5ec15d4439921d4cd05c5e860cb2a41fea
|
||||||
|
ws=workspace([0x3f,0x17,0x79,0x69,0x24,0x6b,0x37,0x50,0x63,0x09,0x45,0x6f,0x0c,0x07,0x07,0x09])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
|
||||||
|
ws.exlookup(d0x6a0be4d0)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V4648(st):
|
||||||
|
#705bd4cd8b61d4596ef4ca40774d68e71f1f846c6e94bd23fd26e5c127e0beaa650a50171f1b
|
||||||
|
ws=workspace([0x16,0x2b,0x64,0x62,0x13,0x04,0x18,0x0d,0x63,0x25,0x14,0x17,0x0f,0x13,0x46,0x0c])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.exlookup(d0x6a0c91a8)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def process_V5683(st):
|
||||||
|
#1f5af733423e5104afb9d5594e682ecf839a776257f33747c9beee671c57ab3f84943f69d8fd
|
||||||
|
ws=workspace([0x7c,0x36,0x5c,0x1a,0x0d,0x10,0x0a,0x50,0x07,0x0f,0x75,0x1f,0x09,0x3b,0x0d,0x72])
|
||||||
|
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
|
||||||
|
remln=len(st)
|
||||||
|
sto=0
|
||||||
|
out=[]
|
||||||
|
while(remln>0):
|
||||||
|
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
|
||||||
|
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
|
||||||
|
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
|
||||||
|
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
|
||||||
|
ws.shuffle(repl)
|
||||||
|
ws.exlookup(d0x6a0d3e80)
|
||||||
|
dat=ws.mask(st[sto:sto+16])
|
||||||
|
out+=dat
|
||||||
|
sto+=16
|
||||||
|
remln-=16
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# def a2hex(arr):
|
||||||
|
# ax=[]
|
||||||
|
# ha="0123456789abcdef"
|
||||||
|
# for a in arr:
|
||||||
|
# if a<0: a=256+a
|
||||||
|
# ax.append(ha[(a>>4)]+ha[a%16])
|
||||||
|
# return "".join(ax)
|
||||||
|
#
|
||||||
|
# def memhex(adr,sz):
|
||||||
|
# emu=EmulatorHelper(currentProgram)
|
||||||
|
# arr=emu.readMemory(getAddress(adr),sz)
|
||||||
|
# return a2hex(arr)
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# obfuscate shared secret according to the VoucherEnvelope version
|
# obfuscate shared secret according to the VoucherEnvelope version
|
||||||
def obfuscate(secret, version):
|
def obfuscate(secret, version):
|
||||||
if version == 1: # v1 does not use obfuscation
|
if version == 1: # v1 does not use obfuscation
|
||||||
@@ -828,6 +1195,107 @@ def obfuscate(secret, version):
|
|||||||
return obfuscated
|
return obfuscated
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# scramble() and obfuscate2() from https://github.com/andrewc12/DeDRM_tools/commit/d9233d61f00d4484235863969919059f4d0b2057
|
||||||
|
|
||||||
|
def scramble(st,magic):
|
||||||
|
ret=bytearray(len(st))
|
||||||
|
padlen=len(st)
|
||||||
|
for counter in range(len(st)):
|
||||||
|
ivar2=(padlen//2)-2*(counter%magic)+magic+counter-1
|
||||||
|
ret[ivar2%padlen]=st[counter]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def obfuscate2(secret, version):
|
||||||
|
if version == 1: # v1 does not use obfuscation
|
||||||
|
return secret
|
||||||
|
magic, word = OBFUSCATION_TABLE["V%d" % version]
|
||||||
|
# extend secret so that its length is divisible by the magic number
|
||||||
|
if len(secret) % magic != 0:
|
||||||
|
secret = secret + b'\x00' * (magic - len(secret) % magic)
|
||||||
|
obfuscated = bytearray(len(secret))
|
||||||
|
wordhash = bytearray(hashlib.sha256(word).digest()[16:])
|
||||||
|
#print(wordhash.hex())
|
||||||
|
shuffled = bytearray(scramble(secret,magic))
|
||||||
|
for i in range(0, len(secret)):
|
||||||
|
obfuscated[i] = shuffled[i] ^ wordhash[i % 16]
|
||||||
|
return obfuscated
|
||||||
|
|
||||||
|
# scramble3() and obfuscate3() from https://github.com/Satsuoni/DeDRM_tools/commit/da6b6a0c911b6d45fe1b13042b690daebc1cc22f
|
||||||
|
|
||||||
|
def scramble3(st,magic):
|
||||||
|
ret=bytearray(len(st))
|
||||||
|
padlen=len(st)
|
||||||
|
divs = padlen // magic
|
||||||
|
cntr = 0
|
||||||
|
iVar6 = 0
|
||||||
|
offset = 0
|
||||||
|
if (0 < ((magic - 1) + divs)):
|
||||||
|
while True:
|
||||||
|
if (offset & 1) == 0 :
|
||||||
|
uVar4 = divs - 1
|
||||||
|
if offset < divs:
|
||||||
|
iVar3 = 0
|
||||||
|
uVar4 = offset
|
||||||
|
else:
|
||||||
|
iVar3 = (offset - divs) + 1
|
||||||
|
if uVar4>=0:
|
||||||
|
iVar5 = uVar4 * magic
|
||||||
|
index = ((padlen - 1) - cntr)
|
||||||
|
while True:
|
||||||
|
if (magic <= iVar3): break
|
||||||
|
ret[index] = st[iVar3 + iVar5]
|
||||||
|
iVar3 = iVar3 + 1
|
||||||
|
cntr = cntr + 1
|
||||||
|
uVar4 = uVar4 - 1
|
||||||
|
iVar5 = iVar5 - magic
|
||||||
|
index -= 1
|
||||||
|
if uVar4<=-1: break
|
||||||
|
else:
|
||||||
|
if (offset < magic):
|
||||||
|
iVar3 = 0
|
||||||
|
else :
|
||||||
|
iVar3 = (offset - magic) + 1
|
||||||
|
if (iVar3 < divs):
|
||||||
|
uVar4 = offset
|
||||||
|
if (magic <= offset):
|
||||||
|
uVar4 = magic - 1
|
||||||
|
|
||||||
|
index = ((padlen - 1) - cntr)
|
||||||
|
iVar5 = iVar3 * magic
|
||||||
|
while True:
|
||||||
|
if (uVar4 < 0) : break
|
||||||
|
iVar3 += 1
|
||||||
|
ret[index] = st[uVar4 + iVar5]
|
||||||
|
uVar4 -= 1
|
||||||
|
index=index-1
|
||||||
|
iVar5 = iVar5 + magic;
|
||||||
|
cntr += 1;
|
||||||
|
if iVar3>=divs: break
|
||||||
|
offset = offset + 1
|
||||||
|
if offset >= ((magic - 1) + divs) :break
|
||||||
|
return ret
|
||||||
|
|
||||||
|
#not sure if the third variant is used anywhere, but it is in Kindle, so I tried to add it
|
||||||
|
def obfuscate3(secret, version):
|
||||||
|
if version == 1: # v1 does not use obfuscation
|
||||||
|
return secret
|
||||||
|
magic, word = OBFUSCATION_TABLE["V%d" % version]
|
||||||
|
# extend secret so that its length is divisible by the magic number
|
||||||
|
if len(secret) % magic != 0:
|
||||||
|
secret = secret + b'\x00' * (magic - len(secret) % magic)
|
||||||
|
#secret = bytearray(secret)
|
||||||
|
obfuscated = bytearray(len(secret))
|
||||||
|
wordhash = bytearray(hashlib.sha256(word).digest())
|
||||||
|
#print(wordhash.hex())
|
||||||
|
shuffled=bytearray(scramble3(secret,magic))
|
||||||
|
#print(shuffled)
|
||||||
|
# shuffle secret and xor it with the first half of the word hash
|
||||||
|
for i in range(0, len(secret)):
|
||||||
|
obfuscated[i] = shuffled[i] ^ wordhash[i % 16]
|
||||||
|
return obfuscated
|
||||||
|
|
||||||
class DrmIonVoucher(object):
|
class DrmIonVoucher(object):
|
||||||
envelope = None
|
envelope = None
|
||||||
version = None
|
version = None
|
||||||
@@ -871,18 +1339,35 @@ class DrmIonVoucher(object):
|
|||||||
else:
|
else:
|
||||||
_assert(False, "Unknown lock parameter: %s" % param)
|
_assert(False, "Unknown lock parameter: %s" % param)
|
||||||
|
|
||||||
sharedsecret = obfuscate(shared, self.version)
|
|
||||||
|
|
||||||
key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
|
# i know that version maps to scramble pretty much 1 to 1, but there was precendent where they changed it, so...
|
||||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
sharedsecrets = [obfuscate(shared, self.version),obfuscate2(shared, self.version),obfuscate3(shared, self.version),
|
||||||
b = aes.decrypt(self.ciphertext)
|
process_V9708(shared), process_V1031(shared), process_V2069(shared), process_V9041(shared),
|
||||||
b = pkcs7unpad(b, 16)
|
process_V3646(shared), process_V6052(shared), process_V9479(shared), process_V9888(shared),
|
||||||
|
process_V4648(shared), process_V5683(shared)]
|
||||||
|
|
||||||
self.drmkey = BinaryIonParser(BytesIO(b))
|
decrypted=False
|
||||||
addprottable(self.drmkey)
|
lastexception = None # type: Exception | None
|
||||||
|
for sharedsecret in sharedsecrets:
|
||||||
|
key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
|
||||||
|
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||||
|
try:
|
||||||
|
b = aes.decrypt(self.ciphertext)
|
||||||
|
b = pkcs7unpad(b, 16)
|
||||||
|
self.drmkey = BinaryIonParser(BytesIO(b))
|
||||||
|
addprottable(self.drmkey)
|
||||||
|
|
||||||
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
|
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
|
||||||
"Expected KeySet, got %s" % self.drmkey.gettypename())
|
"Expected KeySet, got %s" % self.drmkey.gettypename())
|
||||||
|
decrypted=True
|
||||||
|
|
||||||
|
print("Decryption succeeded")
|
||||||
|
break
|
||||||
|
except Exception as ex:
|
||||||
|
lastexception = ex
|
||||||
|
print("Decryption failed, trying next fallback ")
|
||||||
|
if not decrypted:
|
||||||
|
raise lastexception
|
||||||
|
|
||||||
self.drmkey.stepin()
|
self.drmkey.stepin()
|
||||||
while self.drmkey.hasnext():
|
while self.drmkey.hasnext():
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ import getopt
|
|||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
import html.entities
|
try:
|
||||||
|
import html.entities as htmlentitydefs
|
||||||
|
except:
|
||||||
|
import htmlentitydefs
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
@@ -84,68 +88,10 @@ import kgenpids
|
|||||||
import androidkindlekey
|
import androidkindlekey
|
||||||
import kfxdedrm
|
import kfxdedrm
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
from .utilities import SafeUnbuffered
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
from .argv_utils import unicode_argv
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["mobidedrm.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
# cleanup unicode filenames
|
# cleanup unicode filenames
|
||||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
@@ -188,7 +134,7 @@ def unescape(text):
|
|||||||
else:
|
else:
|
||||||
# named entity
|
# named entity
|
||||||
try:
|
try:
|
||||||
text = chr(html.entities.name2codepoint[text[1:-1]])
|
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return text # leave as is
|
return text # leave as is
|
||||||
@@ -215,8 +161,11 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
|||||||
else:
|
else:
|
||||||
mb = topazextract.TopazBook(infile)
|
mb = topazextract.TopazBook(infile)
|
||||||
|
|
||||||
bookname = unescape(mb.getBookTitle())
|
try:
|
||||||
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
|
bookname = unescape(mb.getBookTitle())
|
||||||
|
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
|
||||||
|
except:
|
||||||
|
print("Decrypting {0} ebook.".format(mb.getBookType()))
|
||||||
|
|
||||||
# copy list of pids
|
# copy list of pids
|
||||||
totalpids = list(pids)
|
totalpids = list(pids)
|
||||||
@@ -268,7 +217,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
|
|||||||
orig_fn_root = os.path.splitext(os.path.basename(infile))[0]
|
orig_fn_root = os.path.splitext(os.path.basename(infile))[0]
|
||||||
if (
|
if (
|
||||||
re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or
|
re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or
|
||||||
re.match('^{0-9A-F-}{36}$', orig_fn_root)
|
re.match('^[0-9A-F-]{36}$', orig_fn_root)
|
||||||
): # Kindle for PC / Mac / Android / Fire / iOS
|
): # Kindle for PC / Mac / Android / Fire / iOS
|
||||||
clean_title = cleanup_name(book.getBookTitle())
|
clean_title = cleanup_name(book.getBookTitle())
|
||||||
outfilename = "{}_{}".format(orig_fn_root, clean_title)
|
outfilename = "{}_{}".format(orig_fn_root, clean_title)
|
||||||
@@ -304,7 +253,7 @@ def usage(progname):
|
|||||||
# Main
|
# Main
|
||||||
#
|
#
|
||||||
def cli_main():
|
def cli_main():
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("k4mobidedrm.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
|
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class KFXZipBook:
|
|||||||
# Belt and braces. PIDs should be unicode strings, but just in case...
|
# Belt and braces. PIDs should be unicode strings, but just in case...
|
||||||
if isinstance(pid, bytes):
|
if isinstance(pid, bytes):
|
||||||
pid = pid.decode('ascii')
|
pid = pid.decode('ascii')
|
||||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]:
|
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,0), (32,40), (40,0), (40,40)]:
|
||||||
if len(pid) == dsn_len + secret_len:
|
if len(pid) == dsn_len + secret_len:
|
||||||
break # split pid into DSN and account secret
|
break # split pid into DSN and account secret
|
||||||
else:
|
else:
|
||||||
|
|||||||
5771
DeDRM_plugin/kfxtables.py
Normal file
5771
DeDRM_plugin/kfxtables.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -53,11 +53,17 @@ def SHA1(message):
|
|||||||
def encode(data, map):
|
def encode(data, map):
|
||||||
result = b''
|
result = b''
|
||||||
for char in data:
|
for char in data:
|
||||||
value = char
|
if sys.version_info[0] == 2:
|
||||||
|
value = ord(char)
|
||||||
|
else:
|
||||||
|
value = char
|
||||||
|
|
||||||
Q = (value ^ 0x80) // len(map)
|
Q = (value ^ 0x80) // len(map)
|
||||||
R = value % len(map)
|
R = value % len(map)
|
||||||
result += bytes([map[Q]])
|
|
||||||
result += bytes([map[R]])
|
result += bytes(bytearray([map[Q]]))
|
||||||
|
result += bytes(bytearray([map[R]]))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Hash the bytes in data and then encode the digest with the characters in map
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
@@ -84,7 +90,10 @@ def decode(data,map):
|
|||||||
def getTwoBitsFromBitField(bitField,offset):
|
def getTwoBitsFromBitField(bitField,offset):
|
||||||
byteNumber = offset // 4
|
byteNumber = offset // 4
|
||||||
bitPosition = 6 - 2*(offset % 4)
|
bitPosition = 6 - 2*(offset % 4)
|
||||||
return bitField[byteNumber] >> bitPosition & 3
|
if sys.version_info[0] == 2:
|
||||||
|
return ord(bitField[byteNumber]) >> bitPosition & 3
|
||||||
|
else:
|
||||||
|
return bitField[byteNumber] >> bitPosition & 3
|
||||||
|
|
||||||
# Returns the six bits at offset from a bit field
|
# Returns the six bits at offset from a bit field
|
||||||
def getSixBitsFromBitField(bitField,offset):
|
def getSixBitsFromBitField(bitField,offset):
|
||||||
@@ -97,7 +106,8 @@ def encodePID(hash):
|
|||||||
global charMap3
|
global charMap3
|
||||||
PID = b''
|
PID = b''
|
||||||
for position in range (0,8):
|
for position in range (0,8):
|
||||||
PID += bytes([charMap3[getSixBitsFromBitField(hash,position)]])
|
PID += bytes(bytearray([charMap3[getSixBitsFromBitField(hash,position)]]))
|
||||||
|
|
||||||
return PID
|
return PID
|
||||||
|
|
||||||
# Encryption table used to generate the device PID
|
# Encryption table used to generate the device PID
|
||||||
@@ -134,7 +144,7 @@ def generateDevicePID(table,dsn,nbRoll):
|
|||||||
index = (index+1) %8
|
index = (index+1) %8
|
||||||
for counter in range (0,8):
|
for counter in range (0,8):
|
||||||
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
||||||
pidAscii += bytes([charMap4[index]])
|
pidAscii += bytes(bytearray([charMap4[index]]))
|
||||||
return pidAscii
|
return pidAscii
|
||||||
|
|
||||||
def crc32(s):
|
def crc32(s):
|
||||||
@@ -150,7 +160,7 @@ def checksumPid(s):
|
|||||||
for i in (0,1):
|
for i in (0,1):
|
||||||
b = crc & 0xff
|
b = crc & 0xff
|
||||||
pos = (b // l) ^ (b % l)
|
pos = (b // l) ^ (b % l)
|
||||||
res += bytes([charMap4[pos%l]])
|
res += bytes(bytearray([charMap4[pos%l]]))
|
||||||
crc >>= 8
|
crc >>= 8
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -161,14 +171,17 @@ def pidFromSerial(s, l):
|
|||||||
crc = crc32(s)
|
crc = crc32(s)
|
||||||
arr1 = [0]*l
|
arr1 = [0]*l
|
||||||
for i in range(len(s)):
|
for i in range(len(s)):
|
||||||
arr1[i%l] ^= s[i]
|
if sys.version_info[0] == 2:
|
||||||
|
arr1[i%l] ^= ord(s[i])
|
||||||
|
else:
|
||||||
|
arr1[i%l] ^= s[i]
|
||||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||||
for i in range(l):
|
for i in range(l):
|
||||||
arr1[i] ^= crc_bytes[i&3]
|
arr1[i] ^= crc_bytes[i&3]
|
||||||
pid = b""
|
pid = b""
|
||||||
for i in range(l):
|
for i in range(l):
|
||||||
b = arr1[i] & 0xff
|
b = arr1[i] & 0xff
|
||||||
pid += bytes([charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]])
|
pid += bytes(bytearray([charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]]))
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
|
|
||||||
@@ -177,6 +190,10 @@ def getKindlePids(rec209, token, serialnum):
|
|||||||
if isinstance(serialnum,str):
|
if isinstance(serialnum,str):
|
||||||
serialnum = serialnum.encode('utf-8')
|
serialnum = serialnum.encode('utf-8')
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
if isinstance(serialnum,unicode):
|
||||||
|
serialnum = serialnum.encode('utf-8')
|
||||||
|
|
||||||
if rec209 is None:
|
if rec209 is None:
|
||||||
return [serialnum]
|
return [serialnum]
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,83 +16,25 @@
|
|||||||
import sys
|
import sys
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
from .utilities import SafeUnbuffered
|
||||||
isosx = sys.platform.startswith('darwin')
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
def unicode_argv():
|
letters = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["kindlepid.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
|
||||||
|
|
||||||
def crc32(s):
|
def crc32(s):
|
||||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||||
|
|
||||||
def checksumPid(s):
|
def checksumPid(s):
|
||||||
crc = crc32(s.encode('ascii'))
|
crc = crc32(s)
|
||||||
crc = crc ^ (crc >> 16)
|
crc = crc ^ (crc >> 16)
|
||||||
res = s
|
res = s
|
||||||
l = len(letters)
|
l = len(letters)
|
||||||
for i in (0,1):
|
for i in (0,1):
|
||||||
b = crc & 0xff
|
b = crc & 0xff
|
||||||
pos = (b // l) ^ (b % l)
|
pos = (b // l) ^ (b % l)
|
||||||
res += letters[pos%l]
|
res += bytes(bytearray([letters[pos%l]]))
|
||||||
crc >>= 8
|
crc >>= 8
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@@ -102,22 +44,25 @@ def pidFromSerial(s, l):
|
|||||||
|
|
||||||
arr1 = [0]*l
|
arr1 = [0]*l
|
||||||
for i in range(len(s)):
|
for i in range(len(s)):
|
||||||
arr1[i%l] ^= s[i]
|
if sys.version_info[0] == 2:
|
||||||
|
arr1[i%l] ^= ord(s[i])
|
||||||
|
else:
|
||||||
|
arr1[i%l] ^= s[i]
|
||||||
|
|
||||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||||
for i in range(l):
|
for i in range(l):
|
||||||
arr1[i] ^= crc_bytes[i&3]
|
arr1[i] ^= crc_bytes[i&3]
|
||||||
|
|
||||||
pid = ''
|
pid = b""
|
||||||
for i in range(l):
|
for i in range(l):
|
||||||
b = arr1[i] & 0xff
|
b = arr1[i] & 0xff
|
||||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
pid+=bytes(bytearray([letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]]))
|
||||||
|
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
def cli_main():
|
def cli_main():
|
||||||
print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
|
print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("kindlepid.py")
|
||||||
if len(argv)==2:
|
if len(argv)==2:
|
||||||
serial = argv[1]
|
serial = argv[1]
|
||||||
else:
|
else:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "1.0"
|
__version__ = "1.1"
|
||||||
|
|
||||||
# This is a python script. You need a Python interpreter to run it.
|
# This is a python script. You need a Python interpreter to run it.
|
||||||
# For example, ActiveState Python, which exists for windows.
|
# For example, ActiveState Python, which exists for windows.
|
||||||
@@ -74,78 +74,20 @@ __version__ = "1.0"
|
|||||||
# 0.41 - Fixed potential unicode problem in command line calls
|
# 0.41 - Fixed potential unicode problem in command line calls
|
||||||
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
||||||
# 1.0 - Python 3 compatibility for calibre 5.0
|
# 1.0 - Python 3 compatibility for calibre 5.0
|
||||||
|
# 1.1 - Speed Python PC1 implementation up a little bit
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import binascii
|
import binascii
|
||||||
try:
|
|
||||||
from alfcrypto import Pukall_Cipher
|
|
||||||
except:
|
|
||||||
print("AlfCrypto not found. Using python PC1 implementation.")
|
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
from .alfcrypto import Pukall_Cipher
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
from .utilities import SafeUnbuffered
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
from .argv_utils import unicode_argv
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["mobidedrm.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
@@ -161,55 +103,29 @@ def PC1(key, src, decryption=True):
|
|||||||
# if we can get it from alfcrypto, use that
|
# if we can get it from alfcrypto, use that
|
||||||
try:
|
try:
|
||||||
return Pukall_Cipher().PC1(key,src,decryption)
|
return Pukall_Cipher().PC1(key,src,decryption)
|
||||||
except NameError:
|
except:
|
||||||
pass
|
raise
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# use slow python version, since Pukall_Cipher didn't load
|
letters = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||||
sum1 = 0;
|
|
||||||
sum2 = 0;
|
def crc32(s):
|
||||||
keyXorVal = 0;
|
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||||
if len(key)!=16:
|
|
||||||
DrmException ("PC1: Bad key length")
|
|
||||||
wkey = []
|
|
||||||
for i in range(8):
|
|
||||||
wkey.append(key[i*2]<<8 | key[i*2+1])
|
|
||||||
dst = b''
|
|
||||||
for i in range(len(src)):
|
|
||||||
temp1 = 0;
|
|
||||||
byteXorVal = 0;
|
|
||||||
for j in range(8):
|
|
||||||
temp1 ^= wkey[j]
|
|
||||||
sum2 = (sum2+j)*20021 + sum1
|
|
||||||
sum1 = (temp1*346)&0xFFFF
|
|
||||||
sum2 = (sum2+sum1)&0xFFFF
|
|
||||||
temp1 = (temp1*20021+1)&0xFFFF
|
|
||||||
byteXorVal ^= temp1 ^ sum2
|
|
||||||
curByte = src[i]
|
|
||||||
if not decryption:
|
|
||||||
keyXorVal = curByte * 257;
|
|
||||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
|
||||||
if decryption:
|
|
||||||
keyXorVal = curByte * 257;
|
|
||||||
for j in range(8):
|
|
||||||
wkey[j] ^= keyXorVal;
|
|
||||||
dst+=bytes([curByte])
|
|
||||||
return dst
|
|
||||||
|
|
||||||
# accepts unicode returns unicode
|
|
||||||
def checksumPid(s):
|
def checksumPid(s):
|
||||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
|
||||||
crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF
|
s = s.encode()
|
||||||
|
|
||||||
|
|
||||||
|
crc = crc32(s)
|
||||||
crc = crc ^ (crc >> 16)
|
crc = crc ^ (crc >> 16)
|
||||||
res = s
|
res = s
|
||||||
l = len(letters)
|
l = len(letters)
|
||||||
for i in (0,1):
|
for i in (0,1):
|
||||||
b = crc & 0xff
|
b = crc & 0xff
|
||||||
pos = (b // l) ^ (b % l)
|
pos = (b // l) ^ (b % l)
|
||||||
res += letters[pos%l]
|
res += bytes(bytearray([letters[pos%l]]))
|
||||||
crc >>= 8
|
crc >>= 8
|
||||||
return res
|
return res.decode()
|
||||||
|
|
||||||
# expects bytearray
|
# expects bytearray
|
||||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||||
@@ -218,7 +134,11 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
|
|||||||
if size <= 0:
|
if size <= 0:
|
||||||
return result
|
return result
|
||||||
while True:
|
while True:
|
||||||
v = ptr[size-1]
|
if sys.version_info[0] == 2:
|
||||||
|
v = ord(ptr[size-1])
|
||||||
|
else:
|
||||||
|
v = ptr[size-1]
|
||||||
|
|
||||||
result |= (v & 0x7F) << bitpos
|
result |= (v & 0x7F) << bitpos
|
||||||
bitpos += 7
|
bitpos += 7
|
||||||
size -= 1
|
size -= 1
|
||||||
@@ -234,7 +154,10 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
|
|||||||
# if multibyte data is included in the encryped data, we'll
|
# if multibyte data is included in the encryped data, we'll
|
||||||
# have already cleared this flag.
|
# have already cleared this flag.
|
||||||
if flags & 1:
|
if flags & 1:
|
||||||
num += (ptr[size - num - 1] & 0x3) + 1
|
if sys.version_info[0] == 2:
|
||||||
|
num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||||
|
else:
|
||||||
|
num += (ptr[size - num - 1] & 0x3) + 1
|
||||||
return num
|
return num
|
||||||
|
|
||||||
|
|
||||||
@@ -253,12 +176,7 @@ class MobiBook:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, infile):
|
def __init__(self, infile):
|
||||||
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2022 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
||||||
|
|
||||||
try:
|
|
||||||
from alfcrypto import Pukall_Cipher
|
|
||||||
except:
|
|
||||||
print("AlfCrypto not found. Using python PC1 implementation.")
|
|
||||||
|
|
||||||
# initial sanity check on file
|
# initial sanity check on file
|
||||||
self.data_file = open(infile, 'rb').read()
|
self.data_file = open(infile, 'rb').read()
|
||||||
@@ -398,7 +316,10 @@ class MobiBook:
|
|||||||
for pid in pidlist:
|
for pid in pidlist:
|
||||||
bigpid = pid.encode('utf-8').ljust(16,b'\0')
|
bigpid = pid.encode('utf-8').ljust(16,b'\0')
|
||||||
temp_key = PC1(keyvec1, bigpid, False)
|
temp_key = PC1(keyvec1, bigpid, False)
|
||||||
temp_key_sum = sum(temp_key) & 0xff
|
if sys.version_info[0] == 2:
|
||||||
|
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||||
|
else:
|
||||||
|
temp_key_sum = sum(temp_key) & 0xff
|
||||||
found_key = None
|
found_key = None
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||||
@@ -414,7 +335,11 @@ class MobiBook:
|
|||||||
# Then try the default encoding that doesn't require a PID
|
# Then try the default encoding that doesn't require a PID
|
||||||
pid = '00000000'
|
pid = '00000000'
|
||||||
temp_key = keyvec1
|
temp_key = keyvec1
|
||||||
temp_key_sum = sum(temp_key) & 0xff
|
if sys.version_info[0] == 2:
|
||||||
|
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||||
|
else:
|
||||||
|
temp_key_sum = sum(temp_key) & 0xff
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||||
if cksum == temp_key_sum:
|
if cksum == temp_key_sum:
|
||||||
@@ -452,7 +377,7 @@ class MobiBook:
|
|||||||
if crypto_type == 0:
|
if crypto_type == 0:
|
||||||
print("This book is not encrypted.")
|
print("This book is not encrypted.")
|
||||||
# we must still check for Print Replica
|
# we must still check for Print Replica
|
||||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
self.print_replica = (self.loadSection(1)[0:4] == b'%MOP')
|
||||||
self.mobi_data = self.data_file
|
self.mobi_data = self.data_file
|
||||||
return
|
return
|
||||||
if crypto_type != 2 and crypto_type != 1:
|
if crypto_type != 2 and crypto_type != 1:
|
||||||
@@ -523,7 +448,7 @@ class MobiBook:
|
|||||||
# print "record %d, extra_size %d" %(i,extra_size)
|
# print "record %d, extra_size %d" %(i,extra_size)
|
||||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||||
if i==1:
|
if i==1:
|
||||||
self.print_replica = (decoded_data[0:4] == '%MOP')
|
self.print_replica = (decoded_data[0:4] == b'%MOP')
|
||||||
mobidataList.append(decoded_data)
|
mobidataList.append(decoded_data)
|
||||||
if extra_size > 0:
|
if extra_size > 0:
|
||||||
mobidataList.append(data[-extra_size:])
|
mobidataList.append(data[-extra_size:])
|
||||||
@@ -543,7 +468,7 @@ def getUnencryptedBook(infile,pidlist):
|
|||||||
|
|
||||||
|
|
||||||
def cli_main():
|
def cli_main():
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("mobidedrm.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
if len(argv)<3 or len(argv)>4:
|
if len(argv)<3 or len(argv)>4:
|
||||||
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
# implement just enough of des from openssl to make erdr2pml.py happy
|
|
||||||
|
|
||||||
def load_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
|
|
||||||
if libcrypto is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
# typedef struct DES_ks
|
|
||||||
# {
|
|
||||||
# union
|
|
||||||
# {
|
|
||||||
# DES_cblock cblock;
|
|
||||||
# /* make sure things are correct size on machines with
|
|
||||||
# * 8 byte longs */
|
|
||||||
# DES_LONG deslong[2];
|
|
||||||
# } ks[16];
|
|
||||||
# } DES_key_schedule;
|
|
||||||
|
|
||||||
# just create a big enough place to hold everything
|
|
||||||
# it will have alignment of structure so we should be okay (16 byte aligned?)
|
|
||||||
class DES_KEY_SCHEDULE(Structure):
|
|
||||||
_fields_ = [('DES_cblock1', c_char * 16),
|
|
||||||
('DES_cblock2', c_char * 16),
|
|
||||||
('DES_cblock3', c_char * 16),
|
|
||||||
('DES_cblock4', c_char * 16),
|
|
||||||
('DES_cblock5', c_char * 16),
|
|
||||||
('DES_cblock6', c_char * 16),
|
|
||||||
('DES_cblock7', c_char * 16),
|
|
||||||
('DES_cblock8', c_char * 16),
|
|
||||||
('DES_cblock9', c_char * 16),
|
|
||||||
('DES_cblock10', c_char * 16),
|
|
||||||
('DES_cblock11', c_char * 16),
|
|
||||||
('DES_cblock12', c_char * 16),
|
|
||||||
('DES_cblock13', c_char * 16),
|
|
||||||
('DES_cblock14', c_char * 16),
|
|
||||||
('DES_cblock15', c_char * 16),
|
|
||||||
('DES_cblock16', c_char * 16)]
|
|
||||||
|
|
||||||
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
|
|
||||||
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
|
|
||||||
|
|
||||||
|
|
||||||
class DES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
if len(key) != 8 :
|
|
||||||
raise Exception('DES improper key used')
|
|
||||||
return
|
|
||||||
self.key = key
|
|
||||||
self.keyschedule = DES_KEY_SCHEDULE()
|
|
||||||
DES_set_key(self.key, self.keyschedule)
|
|
||||||
def desdecrypt(self, data):
|
|
||||||
ob = create_string_buffer(len(data))
|
|
||||||
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
|
|
||||||
return ob.raw
|
|
||||||
def decrypt(self, data):
|
|
||||||
if not data:
|
|
||||||
return b''
|
|
||||||
i = 0
|
|
||||||
result = []
|
|
||||||
while i < len(data):
|
|
||||||
block = data[i:i+8]
|
|
||||||
processed_block = self.desdecrypt(block)
|
|
||||||
result.append(processed_block)
|
|
||||||
i += 8
|
|
||||||
return b''.join(result)
|
|
||||||
|
|
||||||
return DES
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
|
|
||||||
def load_pycrypto():
|
|
||||||
try :
|
|
||||||
from Crypto.Cipher import DES as _DES
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
class DES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
if len(key) != 8 :
|
|
||||||
raise ValueError('DES improper key used')
|
|
||||||
self.key = key
|
|
||||||
self._des = _DES.new(key,_DES.MODE_ECB)
|
|
||||||
def desdecrypt(self, data):
|
|
||||||
return self._des.decrypt(data)
|
|
||||||
def decrypt(self, data):
|
|
||||||
if not data:
|
|
||||||
return ''
|
|
||||||
i = 0
|
|
||||||
result = []
|
|
||||||
while i < len(data):
|
|
||||||
block = data[i:i+8]
|
|
||||||
processed_block = self.desdecrypt(block)
|
|
||||||
result.append(processed_block)
|
|
||||||
i += 8
|
|
||||||
return ''.join(result)
|
|
||||||
return DES
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
import sys
|
|
||||||
|
|
||||||
ECB = 0
|
|
||||||
CBC = 1
|
|
||||||
class Des(object):
|
|
||||||
__pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17,
|
|
||||||
9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
|
||||||
62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21,
|
|
||||||
13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3]
|
|
||||||
__left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
|
||||||
__pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9,
|
|
||||||
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
|
||||||
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
|
||||||
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
|
|
||||||
__ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
|
||||||
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
|
||||||
56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2,
|
|
||||||
60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6]
|
|
||||||
__expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8,
|
|
||||||
7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16,
|
|
||||||
15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24,
|
|
||||||
23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0]
|
|
||||||
__sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
|
|
||||||
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
|
||||||
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
|
|
||||||
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
|
|
||||||
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
|
|
||||||
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
|
|
||||||
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
|
|
||||||
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
|
|
||||||
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
|
|
||||||
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
|
||||||
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
|
|
||||||
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
|
|
||||||
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
|
|
||||||
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
|
||||||
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
|
|
||||||
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
|
|
||||||
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
|
|
||||||
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
|
||||||
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
|
|
||||||
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
|
|
||||||
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
|
|
||||||
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
|
||||||
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
|
|
||||||
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
|
|
||||||
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
|
|
||||||
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
|
||||||
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
|
|
||||||
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
|
|
||||||
[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
|
|
||||||
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
|
||||||
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
|
|
||||||
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],]
|
|
||||||
__p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25,
|
|
||||||
4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24]
|
|
||||||
__fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30,
|
|
||||||
37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28,
|
|
||||||
35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26,
|
|
||||||
33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24]
|
|
||||||
# Type of crypting being done
|
|
||||||
ENCRYPT = 0x00
|
|
||||||
DECRYPT = 0x01
|
|
||||||
def __init__(self, key, mode=ECB, IV=None):
|
|
||||||
if len(key) != 8:
|
|
||||||
raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.")
|
|
||||||
self.block_size = 8
|
|
||||||
self.key_size = 8
|
|
||||||
self.__padding = ''
|
|
||||||
self.setMode(mode)
|
|
||||||
if IV:
|
|
||||||
self.setIV(IV)
|
|
||||||
self.L = []
|
|
||||||
self.R = []
|
|
||||||
self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16)
|
|
||||||
self.final = []
|
|
||||||
self.setKey(key)
|
|
||||||
def getKey(self):
|
|
||||||
return self.__key
|
|
||||||
def setKey(self, key):
|
|
||||||
self.__key = key
|
|
||||||
self.__create_sub_keys()
|
|
||||||
def getMode(self):
|
|
||||||
return self.__mode
|
|
||||||
def setMode(self, mode):
|
|
||||||
self.__mode = mode
|
|
||||||
def getIV(self):
|
|
||||||
return self.__iv
|
|
||||||
def setIV(self, IV):
|
|
||||||
if not IV or len(IV) != self.block_size:
|
|
||||||
raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes")
|
|
||||||
self.__iv = IV
|
|
||||||
def getPadding(self):
|
|
||||||
return self.__padding
|
|
||||||
def __String_to_BitList(self, data):
|
|
||||||
l = len(data) * 8
|
|
||||||
result = [0] * l
|
|
||||||
pos = 0
|
|
||||||
for c in data:
|
|
||||||
i = 7
|
|
||||||
ch = ord(c)
|
|
||||||
while i >= 0:
|
|
||||||
if ch & (1 << i) != 0:
|
|
||||||
result[pos] = 1
|
|
||||||
else:
|
|
||||||
result[pos] = 0
|
|
||||||
pos += 1
|
|
||||||
i -= 1
|
|
||||||
return result
|
|
||||||
def __BitList_to_String(self, data):
|
|
||||||
result = ''
|
|
||||||
pos = 0
|
|
||||||
c = 0
|
|
||||||
while pos < len(data):
|
|
||||||
c += data[pos] << (7 - (pos % 8))
|
|
||||||
if (pos % 8) == 7:
|
|
||||||
result += chr(c)
|
|
||||||
c = 0
|
|
||||||
pos += 1
|
|
||||||
return result
|
|
||||||
def __permutate(self, table, block):
|
|
||||||
return [block[x] for x in table]
|
|
||||||
def __create_sub_keys(self):
|
|
||||||
key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey()))
|
|
||||||
i = 0
|
|
||||||
self.L = key[:28]
|
|
||||||
self.R = key[28:]
|
|
||||||
while i < 16:
|
|
||||||
j = 0
|
|
||||||
while j < Des.__left_rotations[i]:
|
|
||||||
self.L.append(self.L[0])
|
|
||||||
del self.L[0]
|
|
||||||
self.R.append(self.R[0])
|
|
||||||
del self.R[0]
|
|
||||||
j += 1
|
|
||||||
self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R)
|
|
||||||
i += 1
|
|
||||||
def __des_crypt(self, block, crypt_type):
|
|
||||||
block = self.__permutate(Des.__ip, block)
|
|
||||||
self.L = block[:32]
|
|
||||||
self.R = block[32:]
|
|
||||||
if crypt_type == Des.ENCRYPT:
|
|
||||||
iteration = 0
|
|
||||||
iteration_adjustment = 1
|
|
||||||
else:
|
|
||||||
iteration = 15
|
|
||||||
iteration_adjustment = -1
|
|
||||||
i = 0
|
|
||||||
while i < 16:
|
|
||||||
tempR = self.R[:]
|
|
||||||
self.R = self.__permutate(Des.__expansion_table, self.R)
|
|
||||||
self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])]
|
|
||||||
B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]]
|
|
||||||
j = 0
|
|
||||||
Bn = [0] * 32
|
|
||||||
pos = 0
|
|
||||||
while j < 8:
|
|
||||||
m = (B[j][0] << 1) + B[j][5]
|
|
||||||
n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4]
|
|
||||||
v = Des.__sbox[j][(m << 4) + n]
|
|
||||||
Bn[pos] = (v & 8) >> 3
|
|
||||||
Bn[pos + 1] = (v & 4) >> 2
|
|
||||||
Bn[pos + 2] = (v & 2) >> 1
|
|
||||||
Bn[pos + 3] = v & 1
|
|
||||||
pos += 4
|
|
||||||
j += 1
|
|
||||||
self.R = self.__permutate(Des.__p, Bn)
|
|
||||||
self.R = [x ^ y for x, y in zip(self.R, self.L)]
|
|
||||||
self.L = tempR
|
|
||||||
i += 1
|
|
||||||
iteration += iteration_adjustment
|
|
||||||
self.final = self.__permutate(Des.__fp, self.R + self.L)
|
|
||||||
return self.final
|
|
||||||
def crypt(self, data, crypt_type):
|
|
||||||
if not data:
|
|
||||||
return ''
|
|
||||||
if len(data) % self.block_size != 0:
|
|
||||||
if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks
|
|
||||||
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.")
|
|
||||||
if not self.getPadding():
|
|
||||||
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character")
|
|
||||||
else:
|
|
||||||
data += (self.block_size - (len(data) % self.block_size)) * self.getPadding()
|
|
||||||
if self.getMode() == CBC:
|
|
||||||
if self.getIV():
|
|
||||||
iv = self.__String_to_BitList(self.getIV())
|
|
||||||
else:
|
|
||||||
raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering")
|
|
||||||
i = 0
|
|
||||||
dict = {}
|
|
||||||
result = []
|
|
||||||
while i < len(data):
|
|
||||||
block = self.__String_to_BitList(data[i:i+8])
|
|
||||||
if self.getMode() == CBC:
|
|
||||||
if crypt_type == Des.ENCRYPT:
|
|
||||||
block = [x ^ y for x, y in zip(block, iv)]
|
|
||||||
processed_block = self.__des_crypt(block, crypt_type)
|
|
||||||
if crypt_type == Des.DECRYPT:
|
|
||||||
processed_block = [x ^ y for x, y in zip(processed_block, iv)]
|
|
||||||
iv = block
|
|
||||||
else:
|
|
||||||
iv = processed_block
|
|
||||||
else:
|
|
||||||
processed_block = self.__des_crypt(block, crypt_type)
|
|
||||||
result.append(self.__BitList_to_String(processed_block))
|
|
||||||
i += 8
|
|
||||||
if crypt_type == Des.DECRYPT and self.getPadding():
|
|
||||||
s = result[-1]
|
|
||||||
while s[-1] == self.getPadding():
|
|
||||||
s = s[:-1]
|
|
||||||
result[-1] = s
|
|
||||||
return ''.join(result)
|
|
||||||
def encrypt(self, data, pad=''):
|
|
||||||
self.__padding = pad
|
|
||||||
return self.crypt(data, Des.ENCRYPT)
|
|
||||||
def decrypt(self, data, pad=''):
|
|
||||||
self.__padding = pad
|
|
||||||
return self.crypt(data, Des.DECRYPT)
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os, os.path
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
class SimplePrefsError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SimplePrefs(object):
|
|
||||||
def __init__(self, target, description):
|
|
||||||
self.prefs = {}
|
|
||||||
self.key2file={}
|
|
||||||
self.file2key={}
|
|
||||||
for keyfilemap in description:
|
|
||||||
[key, filename] = keyfilemap
|
|
||||||
self.key2file[key] = filename
|
|
||||||
self.file2key[filename] = key
|
|
||||||
self.target = target + 'Prefs'
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
try:
|
|
||||||
import winreg
|
|
||||||
except ImportError:
|
|
||||||
import _winreg as winreg
|
|
||||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
|
||||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
|
||||||
prefdir = path + os.sep + self.target
|
|
||||||
elif sys.platform.startswith('darwin'):
|
|
||||||
home = os.getenv('HOME')
|
|
||||||
prefdir = os.path.join(home,'Library','Preferences','org.' + self.target)
|
|
||||||
else:
|
|
||||||
# linux and various flavors of unix
|
|
||||||
home = os.getenv('HOME')
|
|
||||||
prefdir = os.path.join(home,'.' + self.target)
|
|
||||||
if not os.path.exists(prefdir):
|
|
||||||
os.makedirs(prefdir)
|
|
||||||
self.prefdir = prefdir
|
|
||||||
self.prefs['dir'] = self.prefdir
|
|
||||||
self._loadPreferences()
|
|
||||||
|
|
||||||
def _loadPreferences(self):
|
|
||||||
filenames = os.listdir(self.prefdir)
|
|
||||||
for filename in filenames:
|
|
||||||
if filename in self.file2key:
|
|
||||||
key = self.file2key[filename]
|
|
||||||
filepath = os.path.join(self.prefdir,filename)
|
|
||||||
if os.path.isfile(filepath):
|
|
||||||
try :
|
|
||||||
data = file(filepath,'rb').read()
|
|
||||||
self.prefs[key] = data
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def getPreferences(self):
|
|
||||||
return self.prefs
|
|
||||||
|
|
||||||
def setPreferences(self, newprefs={}):
|
|
||||||
if 'dir' not in newprefs:
|
|
||||||
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
|
||||||
if newprefs['dir'] != self.prefs['dir']:
|
|
||||||
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
|
||||||
for key in newprefs:
|
|
||||||
if key != 'dir':
|
|
||||||
if key in self.key2file:
|
|
||||||
filename = self.key2file[key]
|
|
||||||
filepath = os.path.join(self.prefdir,filename)
|
|
||||||
data = newprefs[key]
|
|
||||||
if data != None:
|
|
||||||
data = str(data)
|
|
||||||
if data == None or data == '':
|
|
||||||
if os.path.exists(filepath):
|
|
||||||
os.remove(filepath)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
file(filepath,'wb').write(data)
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
self.prefs = newprefs
|
|
||||||
return
|
|
||||||
@@ -7,6 +7,18 @@ from __future__ import absolute_import, print_function
|
|||||||
|
|
||||||
# Copyright © 2021 NoDRM
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOTE: This code is not functional (yet). I started working on it a while ago
|
||||||
|
to make a standalone version of the plugins that could work without Calibre,
|
||||||
|
too, but for now there's only a rough code structure and no working code yet.
|
||||||
|
|
||||||
|
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
|
||||||
|
change in the future.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
OPT_SHORT_TO_LONG = [
|
OPT_SHORT_TO_LONG = [
|
||||||
["c", "config"],
|
["c", "config"],
|
||||||
["e", "extract"],
|
["e", "extract"],
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ from __future__ import absolute_import, print_function
|
|||||||
|
|
||||||
# Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3
|
# Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOTE: This code is not functional (yet). I started working on it a while ago
|
||||||
|
to make a standalone version of the plugins that could work without Calibre,
|
||||||
|
too, but for now there's only a rough code structure and no working code yet.
|
||||||
|
|
||||||
|
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
|
||||||
|
change in the future.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
import sys, os, codecs, json
|
import sys, os, codecs, json
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ from __future__ import absolute_import, print_function
|
|||||||
|
|
||||||
# Copyright © 2021 NoDRM
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOTE: This code is not functional (yet). I started working on it a while ago
|
||||||
|
to make a standalone version of the plugins that could work without Calibre,
|
||||||
|
too, but for now there's only a rough code structure and no working code yet.
|
||||||
|
|
||||||
|
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
|
||||||
|
change in the future.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ from __future__ import absolute_import, print_function
|
|||||||
|
|
||||||
# Copyright © 2021 NoDRM
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
NOTE: This code is not functional (yet). I started working on it a while ago
|
||||||
|
to make a standalone version of the plugins that could work without Calibre,
|
||||||
|
too, but for now there's only a rough code structure and no working code yet.
|
||||||
|
|
||||||
|
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
|
||||||
|
change in the future.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class DocParser(object):
|
|||||||
print("Scale not defined!")
|
print("Scale not defined!")
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
|
|
||||||
if val == "":
|
if not val:
|
||||||
val = 0
|
val = 0
|
||||||
|
|
||||||
if not ((attr == b'hang') and (int(val) == 0)):
|
if not ((attr == b'hang') and (int(val) == 0)):
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
|
||||||
|
|
||||||
import os, sys
|
|
||||||
import signal
|
|
||||||
import threading
|
|
||||||
import subprocess
|
|
||||||
from subprocess import Popen, PIPE, STDOUT
|
|
||||||
|
|
||||||
# **heavily** chopped up and modfied version of asyncproc.py
|
|
||||||
# to make it actually work on Windows as well as Mac/Linux
|
|
||||||
# For the original see:
|
|
||||||
# "http://www.lysator.liu.se/~bellman/download/"
|
|
||||||
# author is "Thomas Bellman <bellman@lysator.liu.se>"
|
|
||||||
# available under GPL version 3 or Later
|
|
||||||
|
|
||||||
# create an asynchronous subprocess whose output can be collected in
|
|
||||||
# a non-blocking manner
|
|
||||||
|
|
||||||
# What a mess! Have to use threads just to get non-blocking io
|
|
||||||
# in a cross-platform manner
|
|
||||||
|
|
||||||
# luckily all thread use is hidden within this class
|
|
||||||
|
|
||||||
class Process(object):
|
|
||||||
def __init__(self, *params, **kwparams):
|
|
||||||
if len(params) <= 3:
|
|
||||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
|
||||||
if len(params) <= 4:
|
|
||||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
|
||||||
if len(params) <= 5:
|
|
||||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
|
||||||
self.__pending_input = []
|
|
||||||
self.__collected_outdata = []
|
|
||||||
self.__collected_errdata = []
|
|
||||||
self.__exitstatus = None
|
|
||||||
self.__lock = threading.Lock()
|
|
||||||
self.__inputsem = threading.Semaphore(0)
|
|
||||||
self.__quit = False
|
|
||||||
|
|
||||||
self.__process = subprocess.Popen(*params, **kwparams)
|
|
||||||
|
|
||||||
if self.__process.stdin:
|
|
||||||
self.__stdin_thread = threading.Thread(
|
|
||||||
name="stdin-thread",
|
|
||||||
target=self.__feeder, args=(self.__pending_input,
|
|
||||||
self.__process.stdin))
|
|
||||||
self.__stdin_thread.setDaemon(True)
|
|
||||||
self.__stdin_thread.start()
|
|
||||||
|
|
||||||
if self.__process.stdout:
|
|
||||||
self.__stdout_thread = threading.Thread(
|
|
||||||
name="stdout-thread",
|
|
||||||
target=self.__reader, args=(self.__collected_outdata,
|
|
||||||
self.__process.stdout))
|
|
||||||
self.__stdout_thread.setDaemon(True)
|
|
||||||
self.__stdout_thread.start()
|
|
||||||
|
|
||||||
if self.__process.stderr:
|
|
||||||
self.__stderr_thread = threading.Thread(
|
|
||||||
name="stderr-thread",
|
|
||||||
target=self.__reader, args=(self.__collected_errdata,
|
|
||||||
self.__process.stderr))
|
|
||||||
self.__stderr_thread.setDaemon(True)
|
|
||||||
self.__stderr_thread.start()
|
|
||||||
|
|
||||||
def pid(self):
|
|
||||||
return self.__process.pid
|
|
||||||
|
|
||||||
def kill(self, signal):
|
|
||||||
self.__process.send_signal(signal)
|
|
||||||
|
|
||||||
# check on subprocess (pass in 'nowait') to act like poll
|
|
||||||
def wait(self, flag):
|
|
||||||
if flag.lower() == 'nowait':
|
|
||||||
rc = self.__process.poll()
|
|
||||||
else:
|
|
||||||
rc = self.__process.wait()
|
|
||||||
if rc != None:
|
|
||||||
if self.__process.stdin:
|
|
||||||
self.closeinput()
|
|
||||||
if self.__process.stdout:
|
|
||||||
self.__stdout_thread.join()
|
|
||||||
if self.__process.stderr:
|
|
||||||
self.__stderr_thread.join()
|
|
||||||
return self.__process.returncode
|
|
||||||
|
|
||||||
def terminate(self):
|
|
||||||
if self.__process.stdin:
|
|
||||||
self.closeinput()
|
|
||||||
self.__process.terminate()
|
|
||||||
|
|
||||||
# thread gets data from subprocess stdout
|
|
||||||
def __reader(self, collector, source):
|
|
||||||
while True:
|
|
||||||
data = os.read(source.fileno(), 65536)
|
|
||||||
self.__lock.acquire()
|
|
||||||
collector.append(data)
|
|
||||||
self.__lock.release()
|
|
||||||
if data == "":
|
|
||||||
source.close()
|
|
||||||
break
|
|
||||||
return
|
|
||||||
|
|
||||||
# thread feeds data to subprocess stdin
|
|
||||||
def __feeder(self, pending, drain):
|
|
||||||
while True:
|
|
||||||
self.__inputsem.acquire()
|
|
||||||
self.__lock.acquire()
|
|
||||||
if not pending and self.__quit:
|
|
||||||
drain.close()
|
|
||||||
self.__lock.release()
|
|
||||||
break
|
|
||||||
data = pending.pop(0)
|
|
||||||
self.__lock.release()
|
|
||||||
drain.write(data)
|
|
||||||
|
|
||||||
# non-blocking read of data from subprocess stdout
|
|
||||||
def read(self):
|
|
||||||
self.__lock.acquire()
|
|
||||||
outdata = "".join(self.__collected_outdata)
|
|
||||||
del self.__collected_outdata[:]
|
|
||||||
self.__lock.release()
|
|
||||||
return outdata
|
|
||||||
|
|
||||||
# non-blocking read of data from subprocess stderr
|
|
||||||
def readerr(self):
|
|
||||||
self.__lock.acquire()
|
|
||||||
errdata = "".join(self.__collected_errdata)
|
|
||||||
del self.__collected_errdata[:]
|
|
||||||
self.__lock.release()
|
|
||||||
return errdata
|
|
||||||
|
|
||||||
# non-blocking write to stdin of subprocess
|
|
||||||
def write(self, data):
|
|
||||||
if self.__process.stdin is None:
|
|
||||||
raise ValueError("Writing to process with stdin not a pipe")
|
|
||||||
self.__lock.acquire()
|
|
||||||
self.__pending_input.append(data)
|
|
||||||
self.__inputsem.release()
|
|
||||||
self.__lock.release()
|
|
||||||
|
|
||||||
# close stdinput of subprocess
|
|
||||||
def closeinput(self):
|
|
||||||
self.__lock.acquire()
|
|
||||||
self.__quit = True
|
|
||||||
self.__inputsem.release()
|
|
||||||
self.__lock.release()
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
# topazextract.py
|
# topazextract.py
|
||||||
# Mostly written by some_updates based on code from many others
|
# Mostly written by some_updates based on code from many others
|
||||||
|
|
||||||
@@ -22,70 +24,11 @@ import traceback
|
|||||||
from struct import pack
|
from struct import pack
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
from alfcrypto import Topaz_Cipher
|
from .alfcrypto import Topaz_Cipher
|
||||||
|
from .utilities import SafeUnbuffered
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
from .argv_utils import unicode_argv
|
||||||
# and also make sure that any unicode strings get
|
|
||||||
# encoded using "replace" before writing them.
|
|
||||||
class SafeUnbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
self.encoding = stream.encoding
|
|
||||||
if self.encoding == None:
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
def write(self, data):
|
|
||||||
if isinstance(data,str) or isinstance(data,unicode):
|
|
||||||
# str for Python3, unicode for Python2
|
|
||||||
data = data.encode(self.encoding,"replace")
|
|
||||||
try:
|
|
||||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
|
||||||
# self.stream.buffer for Python3, self.stream for Python2
|
|
||||||
buffer.write(data)
|
|
||||||
buffer.flush()
|
|
||||||
except:
|
|
||||||
# We can do nothing if a write fails
|
|
||||||
raise
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
|
||||||
isosx = sys.platform.startswith('darwin')
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
if iswindows:
|
|
||||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
|
||||||
# strings.
|
|
||||||
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
|
|
||||||
|
|
||||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
|
||||||
GetCommandLineW.argtypes = []
|
|
||||||
GetCommandLineW.restype = LPCWSTR
|
|
||||||
|
|
||||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
|
||||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = GetCommandLineW()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in
|
|
||||||
range(start, argc.value)]
|
|
||||||
# if we don't have any arguments at all, just pass back script name
|
|
||||||
# this should never happen
|
|
||||||
return ["mobidedrm.py"]
|
|
||||||
else:
|
|
||||||
argvencoding = sys.stdin.encoding or "utf-8"
|
|
||||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
|
||||||
|
|
||||||
#global switch
|
#global switch
|
||||||
debug = False
|
debug = False
|
||||||
@@ -458,7 +401,7 @@ def usage(progname):
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
def cli_main():
|
def cli_main():
|
||||||
argv=unicode_argv()
|
argv=unicode_argv("topazextract.py")
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
print("TopazExtract v{0}.".format(__version__))
|
print("TopazExtract v{0}.".format(__version__))
|
||||||
|
|
||||||
|
|||||||
@@ -3,39 +3,47 @@
|
|||||||
|
|
||||||
#@@CALIBRE_COMPAT_CODE@@
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
from ignoblekeyGenPassHash import generate_key
|
import sys
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
DETAILED_MESSAGE = \
|
|
||||||
'You have personal information stored in this plugin\'s customization '+ \
|
|
||||||
'string from a previous version of this plugin.\n\n'+ \
|
|
||||||
'This new version of the plugin can convert that info '+ \
|
|
||||||
'into key data that the new plugin can then use (which doesn\'t '+ \
|
|
||||||
'require personal information to be stored/displayed in an insecure '+ \
|
|
||||||
'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
|
|
||||||
'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
|
|
||||||
'to manually re-configure this plugin with your information.\n\nEither way... ' + \
|
|
||||||
'this new version of the plugin will not be responsible for storing that personal '+ \
|
|
||||||
'info in plain sight any longer.'
|
|
||||||
|
|
||||||
def uStrCmp (s1, s2, caseless=False):
|
def uStrCmp (s1, s2, caseless=False):
|
||||||
import unicodedata as ud
|
import unicodedata as ud
|
||||||
str1 = s1 if isinstance(s1, str) else str(s1)
|
if sys.version_info[0] == 2:
|
||||||
str2 = s2 if isinstance(s2, str) else str(s2)
|
str1 = s1 if isinstance(s1, unicode) else unicode(s1)
|
||||||
|
str2 = s2 if isinstance(s2, unicode) else unicode(s2)
|
||||||
|
else:
|
||||||
|
str1 = s1 if isinstance(s1, str) else str(s1)
|
||||||
|
str2 = s2 if isinstance(s2, str) else str(s2)
|
||||||
|
|
||||||
if caseless:
|
if caseless:
|
||||||
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
|
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
|
||||||
else:
|
else:
|
||||||
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
|
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
|
||||||
|
|
||||||
def parseCustString(keystuff):
|
|
||||||
userkeys = []
|
|
||||||
ar = keystuff.split(':')
|
# Wrap a stream so that output gets flushed immediately
|
||||||
for i in ar:
|
# and also make sure that any unicode strings get safely
|
||||||
|
# encoded using "replace" before writing them.
|
||||||
|
class SafeUnbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
try:
|
try:
|
||||||
name, ccn = i.split(',')
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
userkeys.append(generate_key(name, ccn))
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
except:
|
except:
|
||||||
pass
|
# We can do nothing if a write fails
|
||||||
return userkeys
|
raise
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
|||||||
pyexec = WinePythonCLI(wineprefix)
|
pyexec = WinePythonCLI(wineprefix)
|
||||||
except NoWinePython3Exception:
|
except NoWinePython3Exception:
|
||||||
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
|
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
basepath, script = os.path.split(scriptpath)
|
basepath, script = os.path.split(scriptpath)
|
||||||
print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script))
|
print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script))
|
||||||
|
|||||||
30
DeDRM_plugin/zeroedzipinfo.py
Normal file
30
DeDRM_plugin/zeroedzipinfo.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Python 3's "zipfile" has an annoying bug where the `external_attr` field
|
||||||
|
of a ZIP file cannot be set to 0. However, if the original DRMed ZIP has
|
||||||
|
that set to 0 then we want the DRM-free ZIP to have that as 0, too.
|
||||||
|
See https://github.com/python/cpython/issues/87713
|
||||||
|
|
||||||
|
We cannot just set the "external_attr" to 0 as the code to save the ZIP
|
||||||
|
resets that variable.
|
||||||
|
|
||||||
|
So, here's a class that inherits from ZipInfo and ensures that EVERY
|
||||||
|
read access to that variable will return a 0 ...
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
class ZeroedZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, zinfo):
|
||||||
|
for k in self.__slots__:
|
||||||
|
if hasattr(zinfo, k):
|
||||||
|
setattr(self, k, getattr(zinfo, k))
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
if name == "external_attr":
|
||||||
|
return 0
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
@@ -394,6 +394,19 @@ class ZipInfo (object):
|
|||||||
extra = extra[ln+4:]
|
extra = extra[ln+4:]
|
||||||
|
|
||||||
|
|
||||||
|
class ZeroedZipInfo(ZipInfo):
|
||||||
|
def __init__(self, zinfo):
|
||||||
|
for k in self.__slots__:
|
||||||
|
if hasattr(zinfo, k):
|
||||||
|
setattr(self, k, getattr(zinfo, k))
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
if name == "external_attr":
|
||||||
|
return 0
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class _ZipDecrypter:
|
class _ZipDecrypter:
|
||||||
"""Class to handle decryption of files stored within a ZIP archive.
|
"""Class to handle decryption of files stored within a ZIP archive.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import sys, os
|
|||||||
|
|
||||||
import zlib
|
import zlib
|
||||||
import zipfilerugged
|
import zipfilerugged
|
||||||
|
from zipfilerugged import ZipInfo, ZeroedZipInfo
|
||||||
import getopt
|
import getopt
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
@@ -36,12 +37,6 @@ _FILENAME_OFFSET = 30
|
|||||||
_MAX_SIZE = 64 * 1024
|
_MAX_SIZE = 64 * 1024
|
||||||
_MIMETYPE = 'application/epub+zip'
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
class ZipInfo(zipfilerugged.ZipInfo):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'compress_type' in kwargs:
|
|
||||||
compress_type = kwargs.pop('compress_type')
|
|
||||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
|
||||||
self.compress_type = compress_type
|
|
||||||
|
|
||||||
class fixZip:
|
class fixZip:
|
||||||
def __init__(self, zinput, zoutput):
|
def __init__(self, zinput, zoutput):
|
||||||
@@ -117,7 +112,8 @@ class fixZip:
|
|||||||
# if epub write mimetype file first, with no compression
|
# if epub write mimetype file first, with no compression
|
||||||
if self.ztype == 'epub':
|
if self.ztype == 'epub':
|
||||||
# first get a ZipInfo with current time and no compression
|
# first get a ZipInfo with current time and no compression
|
||||||
mimeinfo = ZipInfo(b'mimetype',compress_type=zipfilerugged.ZIP_STORED)
|
mimeinfo = ZipInfo(b'mimetype')
|
||||||
|
mimeinfo.compress_type = zipfilerugged.ZIP_STORED
|
||||||
mimeinfo.internal_attr = 1 # text file
|
mimeinfo.internal_attr = 1 # text file
|
||||||
try:
|
try:
|
||||||
# if the mimetype is present, get its info, including time-stamp
|
# if the mimetype is present, get its info, including time-stamp
|
||||||
@@ -129,8 +125,16 @@ class fixZip:
|
|||||||
mimeinfo.internal_attr = oldmimeinfo.internal_attr
|
mimeinfo.internal_attr = oldmimeinfo.internal_attr
|
||||||
mimeinfo.external_attr = oldmimeinfo.external_attr
|
mimeinfo.external_attr = oldmimeinfo.external_attr
|
||||||
mimeinfo.create_system = oldmimeinfo.create_system
|
mimeinfo.create_system = oldmimeinfo.create_system
|
||||||
|
mimeinfo.create_version = oldmimeinfo.create_version
|
||||||
|
mimeinfo.volume = oldmimeinfo.volume
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if mimeinfo.external_attr == 0:
|
||||||
|
mimeinfo = ZeroedZipInfo(mimeinfo)
|
||||||
|
|
||||||
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
|
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
|
||||||
|
|
||||||
# write the rest of the files
|
# write the rest of the files
|
||||||
@@ -145,13 +149,23 @@ class fixZip:
|
|||||||
zinfo.filename = local_name
|
zinfo.filename = local_name
|
||||||
|
|
||||||
# create new ZipInfo with only the useful attributes from the old info
|
# create new ZipInfo with only the useful attributes from the old info
|
||||||
nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
|
nzinfo = ZipInfo(zinfo.filename)
|
||||||
|
nzinfo.date_time = zinfo.date_time
|
||||||
|
nzinfo.compress_type = zinfo.compress_type
|
||||||
nzinfo.comment=zinfo.comment
|
nzinfo.comment=zinfo.comment
|
||||||
nzinfo.extra=zinfo.extra
|
nzinfo.extra=zinfo.extra
|
||||||
nzinfo.internal_attr=zinfo.internal_attr
|
nzinfo.internal_attr=zinfo.internal_attr
|
||||||
nzinfo.external_attr=zinfo.external_attr
|
nzinfo.external_attr=zinfo.external_attr
|
||||||
nzinfo.create_system=zinfo.create_system
|
nzinfo.create_system=zinfo.create_system
|
||||||
|
nzinfo.create_version = zinfo.create_version
|
||||||
|
nzinfo.volume = zinfo.volume
|
||||||
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
|
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
|
||||||
|
|
||||||
|
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
|
||||||
|
# if it's NULL, so we need a workaround:
|
||||||
|
if nzinfo.external_attr == 0:
|
||||||
|
nzinfo = ZeroedZipInfo(nzinfo)
|
||||||
|
|
||||||
self.outzip.writestr(nzinfo,data)
|
self.outzip.writestr(nzinfo,data)
|
||||||
|
|
||||||
self.bzf.close()
|
self.bzf.close()
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ DeDRM_plugin.zip
|
|||||||
This plugin will remove the DRM from:
|
This plugin will remove the DRM from:
|
||||||
|
|
||||||
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
|
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
|
||||||
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
- Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||||
- Adobe Digital Editions (v2.0.1) PDFs
|
- Adobe Digital Editions PDFs
|
||||||
|
|
||||||
For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
|
For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
|
||||||
|
|
||||||
|
|||||||
31
FAQs.md
31
FAQs.md
@@ -9,14 +9,11 @@ DRM ("Digital Rights Management") is a way of using encryption to tie the books
|
|||||||
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
|
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
|
||||||
|
|
||||||
## So how can I remove DRM from my ebooks?
|
## So how can I remove DRM from my ebooks?
|
||||||
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a, a couple of quid pro quos.
|
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a couple of quid pro quos.
|
||||||
|
|
||||||
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
|
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
|
||||||
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
||||||
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
||||||
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
|
|
||||||
|
|
||||||
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
|
|
||||||
|
|
||||||
### Recent Changes to Kindle for PC/Kindle for Mac
|
### Recent Changes to Kindle for PC/Kindle for Mac
|
||||||
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
|
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
|
||||||
@@ -34,7 +31,7 @@ Verify the one of the following cryptographic hash values, using software of you
|
|||||||
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
||||||
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
|
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
|
||||||
|
|
||||||
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest wayis to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/ just delete the folder 'updates' and save a blank text file called 'updates' in its place.
|
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest way is to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/. Make the 'updates' folder read-only, or delete it and save a blank text file called 'updates' in its place.
|
||||||
|
|
||||||
Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running:
|
Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running:
|
||||||
|
|
||||||
@@ -46,9 +43,9 @@ PC Note: The renderer-test program may be in a different location in some Kindle
|
|||||||
#### Macintosh
|
#### Macintosh
|
||||||
`chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test`
|
`chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test`
|
||||||
|
|
||||||
Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]
|
Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]. This only works on Kindle for Mac 1.19 thru 1.31, it does NOT work with 1.32 or newer.
|
||||||
|
|
||||||
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
|
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books should be downloaded by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
|
||||||
|
|
||||||
#### Decrypting KFX
|
#### Decrypting KFX
|
||||||
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
|
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
|
||||||
@@ -59,18 +56,18 @@ It's quite possible that Amazon will update their KFX DeDRM to prevent DRM remov
|
|||||||
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
|
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
|
||||||
|
|
||||||
## Where can I get the latest version of these free DRM removal tools?
|
## Where can I get the latest version of these free DRM removal tools?
|
||||||
Right here at github. Just go to the [releases page](https://github.com/noDRM/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. This will get you the forked version by noDRM. If you want to download the original version by Apprentice Harper, go to [this page](https://github.com/noDRM/DeDRM_tools/releases) instead.
|
Right here at github. Just go to the [releases page](https://github.com/noDRM/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. This will get you the forked version by noDRM. If you want to download the original version by Apprentice Harper, go to [this page](https://github.com/apprenticeharper/DeDRM_tools/releases) instead.
|
||||||
|
|
||||||
## I've downloaded the tools archive. Now what?
|
## I've downloaded the tools archive. Now what?
|
||||||
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
|
First, unzip the archive. You should now have a DeDRM folder containing several files, including a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the files are, and you'll be able to work out which of the tools you need.
|
||||||
|
|
||||||
## That's a big complicated ReadMe file! Isn't there a quick guide?
|
## That's a big complicated ReadMe file! Isn't there a quick guide?
|
||||||
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers. Remember that the plugin only tries to remove DRM when ebooks are imported.
|
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers. Remember that the plugin only tries to remove DRM when ebooks are imported.
|
||||||
|
|
||||||
# Installing the Tools
|
# Installing the Tools
|
||||||
## The calibre plugin
|
## The calibre plugin
|
||||||
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u’[path]DeDRM\_tools\_6.8.0.zip’ is invalid. It does not contain a top-level \_\_init\_\_.py file"
|
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in '[path]DeDRM\_tools\_X.X.X.zip' is invalid. It does not contain a top-level \_\_init\_\_.py file"
|
||||||
You are trying to add the tools archive (e.g. `DeDRM_tools_6.8.0.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from a folder called `DeDRM_calibre_plugin` in the unzipped archive.
|
You are trying to add the tools archive (e.g. `DeDRM_tools_10.0.2.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from inside the unzipped archive.
|
||||||
|
|
||||||
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
|
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
|
||||||
You should select the zip file that is in the `DeDRM_calibre_plugin` folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
|
You should select the zip file that is in the `DeDRM_calibre_plugin` folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
|
||||||
@@ -133,7 +130,7 @@ If the book is from Kindle for PC or Kindle for Mac and you think you are doing
|
|||||||
There are several possible reasons why only some books get their DRM removed.
|
There are several possible reasons why only some books get their DRM removed.
|
||||||
* You still don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
|
* You still don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
|
||||||
|
|
||||||
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harpers version, post that logfile as a comment at Apprentice Alf's blog or in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
|
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harper's version, post that logfile in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
|
||||||
|
|
||||||
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
|
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
|
||||||
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
|
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
|
||||||
@@ -144,11 +141,8 @@ You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper.
|
|||||||
## Do the tools work on books from Kobo?
|
## Do the tools work on books from Kobo?
|
||||||
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
||||||
|
|
||||||
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
|
|
||||||
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
|
|
||||||
|
|
||||||
## I cannot solve my problem with the DeDRM plugin, and now I need to ‘post a log’. How do I do that?
|
## I cannot solve my problem with the DeDRM plugin, and now I need to ‘post a log’. How do I do that?
|
||||||
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
|
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in NoDRM’s GitHub repo. If you're using Apprentice Harper’s version, post that logfile in a new issue at [Apprentice Harper's GitHub repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
|
||||||
|
|
||||||
## Is there a way to use the DeDRM plugin for Calibre from the command line?
|
## Is there a way to use the DeDRM plugin for Calibre from the command line?
|
||||||
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
|
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
|
||||||
@@ -189,16 +183,17 @@ Amazon turned off backup for Kindle for Android, so the tools can no longer find
|
|||||||
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
||||||
|
|
||||||
## Why don't the tools work with LCP-encrypted ebooks? / Error message about a "DMCA takedown"
|
## Why don't the tools work with LCP-encrypted ebooks? / Error message about a "DMCA takedown"
|
||||||
Support for LCP DRM removal was included in the past, but Readium (the company who developed that particular DRM) has decided to [open a DMCA takedown request](https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md) in January 2022. This means that for legal reasons, this GitHun repository no longer contains the code needed to remove DRM from LCP-encrypted books. For more information please read [this bug report](https://github.com/noDRM/DeDRM_tools/issues/18).
|
Support for LCP DRM removal was included in the past, but Readium (the company who developed that particular DRM) has decided to [open a DMCA takedown request](https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md) in January 2022. This means that for legal reasons, this GitHub repository no longer contains the code needed to remove DRM from LCP-encrypted books. For more information please read [this bug report](https://github.com/noDRM/DeDRM_tools/issues/18).
|
||||||
|
|
||||||
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
||||||
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
|
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
|
||||||
* Read the ReadMe file for the tool you want to use.
|
* Read the ReadMe file for the tool you want to use.
|
||||||
* If you still can’t remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
|
* If you still can’t remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
|
||||||
|
|
||||||
## Who wrote these scripts?
|
## Who wrote these scripts?
|
||||||
The authors tend to identify themselves only by pseudonyms:
|
The authors tend to identify themselves only by pseudonyms:
|
||||||
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
||||||
|
* The Adobe Adept support for ADE3.0+ DRM was added by a980e066a01
|
||||||
* ~The Readium LCP support for this plugin was created by NoDRM~ (removed due to a DMCA takedown, see [#18](https://github.com/noDRM/DeDRM_tools/issues/18) )
|
* ~The Readium LCP support for this plugin was created by NoDRM~ (removed due to a DMCA takedown, see [#18](https://github.com/noDRM/DeDRM_tools/issues/18) )
|
||||||
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
|
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
|
||||||
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
|
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
print_function)
|
print_function)
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '10.0.0'
|
__version__ = '10.0.9'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
#####################################################################
|
#####################################################################
|
||||||
@@ -20,7 +20,7 @@ except NameError:
|
|||||||
PLUGIN_NAME = 'Obok DeDRM'
|
PLUGIN_NAME = 'Obok DeDRM'
|
||||||
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||||
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
|
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
|
||||||
PLUGIN_VERSION_TUPLE = (10, 0, 0)
|
PLUGIN_VERSION_TUPLE = (10, 0, 9)
|
||||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||||
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
||||||
PLUGIN_AUTHORS = 'Anon'
|
PLUGIN_AUTHORS = 'Anon'
|
||||||
|
|||||||
@@ -237,7 +237,10 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
|
|
||||||
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
||||||
'''
|
'''
|
||||||
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
|
|
||||||
|
cfg_add_duplicates = (cfg['finding_homes_for_formats'] == 'Add new entry')
|
||||||
|
|
||||||
|
added = self.db.add_books(books_to_add, add_duplicates=cfg_add_duplicates, run_hooks=False)
|
||||||
if len(added[0]):
|
if len(added[0]):
|
||||||
# Record the id(s) that got added
|
# Record the id(s) that got added
|
||||||
for id in added[0]:
|
for id in added[0]:
|
||||||
@@ -371,7 +374,11 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
result['success'] = False
|
result['success'] = False
|
||||||
result['fileobj'] = None
|
result['fileobj'] = None
|
||||||
|
|
||||||
zin = zipfile.ZipFile(book.filename, 'r')
|
try:
|
||||||
|
zin = zipfile.ZipFile(book.filename, 'r')
|
||||||
|
except FileNotFoundError:
|
||||||
|
print (_('{0} - File "{1}" not found. Make sure the eBook has been properly downloaded in the Kobo app.').format(PLUGIN_NAME, book.filename))
|
||||||
|
return result
|
||||||
#print ('Kobo library filename: {0}'.format(book.filename))
|
#print ('Kobo library filename: {0}'.format(book.filename))
|
||||||
for userkey in self.userkeys:
|
for userkey in self.userkeys:
|
||||||
print (_('Trying key: '), codecs.encode(userkey, 'hex'))
|
print (_('Trying key: '), codecs.encode(userkey, 'hex'))
|
||||||
|
|||||||
@@ -265,13 +265,13 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
|||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
if text is None:
|
if text is None:
|
||||||
text = ''
|
text = ''
|
||||||
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|
||||||
class RatingTableWidgetItem(QTableWidgetItem):
|
class RatingTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
def __init__(self, rating, is_read_only=False):
|
def __init__(self, rating, is_read_only=False):
|
||||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
|
||||||
self.setData(Qt.DisplayRole, rating)
|
self.setData(Qt.DisplayRole, rating)
|
||||||
if is_read_only:
|
if is_read_only:
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
@@ -284,11 +284,11 @@ class DateTableWidgetItem(QTableWidgetItem):
|
|||||||
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
|
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
|
||||||
date_read = now()
|
date_read = now()
|
||||||
if is_read_only:
|
if is_read_only:
|
||||||
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.ItemType.UserType)
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||||
else:
|
else:
|
||||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
|
||||||
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||||
|
|
||||||
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
|
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
|
||||||
|
|||||||
@@ -39,8 +39,13 @@ class ConfigWidget(QWidget):
|
|||||||
self.find_homes = QComboBox()
|
self.find_homes = QComboBox()
|
||||||
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
|
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
|
||||||
layout.addWidget(self.find_homes)
|
layout.addWidget(self.find_homes)
|
||||||
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
|
|
||||||
|
self.find_homes.addItems([_('Ask'), _('Always'), _('Never'), _('Add new entry')])
|
||||||
|
|
||||||
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
|
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
|
||||||
|
if index == -1:
|
||||||
|
index = self.find_homes.findText(_(plugin_prefs['finding_homes_for_formats']))
|
||||||
|
|
||||||
self.find_homes.setCurrentIndex(index)
|
self.find_homes.setCurrentIndex(index)
|
||||||
|
|
||||||
self.serials_button = QtGui.QPushButton(self)
|
self.serials_button = QtGui.QPushButton(self)
|
||||||
@@ -62,14 +67,31 @@ class ConfigWidget(QWidget):
|
|||||||
|
|
||||||
|
|
||||||
def edit_kobo_directory(self):
|
def edit_kobo_directory(self):
|
||||||
tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
|
tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.Option.ShowDirsOnly)
|
||||||
|
|
||||||
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
|
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
|
||||||
self.kobodirectory = tmpkobodirectory
|
self.kobodirectory = tmpkobodirectory
|
||||||
|
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
plugin_prefs['finding_homes_for_formats'] = self.find_homes.currentText()
|
|
||||||
|
|
||||||
|
# Make sure the config file string is *always* english.
|
||||||
|
find_homes = None
|
||||||
|
if self.find_homes.currentText() == _('Ask'):
|
||||||
|
find_homes = 'Ask'
|
||||||
|
elif self.find_homes.currentText() == _('Always'):
|
||||||
|
find_homes = 'Always'
|
||||||
|
elif self.find_homes.currentText() == _('Never'):
|
||||||
|
find_homes = 'Never'
|
||||||
|
elif self.find_homes.currentText() == _('Add new entry'):
|
||||||
|
find_homes = 'Add new entry'
|
||||||
|
|
||||||
|
if find_homes is None:
|
||||||
|
# Fallback
|
||||||
|
find_homes = self.find_homes.currentText()
|
||||||
|
|
||||||
|
plugin_prefs['finding_homes_for_formats'] = find_homes
|
||||||
plugin_prefs['kobo_serials'] = self.tmpserials
|
plugin_prefs['kobo_serials'] = self.tmpserials
|
||||||
plugin_prefs['kobo_directory'] = self.kobodirectory
|
plugin_prefs['kobo_directory'] = self.kobodirectory
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
|||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
if text is None:
|
if text is None:
|
||||||
text = ''
|
text = ''
|
||||||
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|
||||||
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
|
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
|
||||||
@@ -448,7 +448,7 @@ class IconWidgetItem(ReadOnlyTableWidgetItem):
|
|||||||
class NumericTableWidgetItem(QTableWidgetItem):
|
class NumericTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
def __init__(self, number, is_read_only=False):
|
def __init__(self, number, is_read_only=False):
|
||||||
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
|
||||||
self.setData(Qt.DisplayRole, number)
|
self.setData(Qt.DisplayRole, number)
|
||||||
if is_read_only:
|
if is_read_only:
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Version 10.0.3 July 2022
|
||||||
|
# Fix Calibre 6
|
||||||
|
#
|
||||||
|
# Version 10.0.1 February 2022
|
||||||
|
# Remove OpenSSL support to only support PyCryptodome; clean up the code.
|
||||||
|
#
|
||||||
# Version 10.0.0 November 2021
|
# Version 10.0.0 November 2021
|
||||||
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
|
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
|
||||||
# key fetch issues on some machines.
|
# key fetch issues on some machines.
|
||||||
@@ -162,8 +168,8 @@
|
|||||||
"""Manage all Kobo books, either encrypted or DRM-free."""
|
"""Manage all Kobo books, either encrypted or DRM-free."""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
__version__ = '4.0.0'
|
__version__ = '10.0.9'
|
||||||
__about__ = "Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__)
|
__about__ = "Obok v{0}\nCopyright © 2012-2023 Physisticated et al.".format(__version__)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -180,6 +186,20 @@ import shutil
|
|||||||
import argparse
|
import argparse
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
except ImportError:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
def unpad(data, padding=16):
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
pad_len = ord(data[-1])
|
||||||
|
else:
|
||||||
|
pad_len = data[-1]
|
||||||
|
|
||||||
|
return data[:-pad_len]
|
||||||
|
|
||||||
|
|
||||||
can_parse_xml = True
|
can_parse_xml = True
|
||||||
try:
|
try:
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@@ -194,88 +214,6 @@ KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
|
|||||||
class ENCRYPTIONError(Exception):
|
class ENCRYPTIONError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _load_crypto_libcrypto():
|
|
||||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
|
||||||
Structure, c_ulong, create_string_buffer, cast
|
|
||||||
from ctypes.util import find_library
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
libcrypto = find_library('libeay32')
|
|
||||||
else:
|
|
||||||
libcrypto = find_library('crypto')
|
|
||||||
|
|
||||||
if libcrypto is None:
|
|
||||||
raise ENCRYPTIONError('libcrypto not found')
|
|
||||||
libcrypto = CDLL(libcrypto)
|
|
||||||
|
|
||||||
AES_MAXNR = 14
|
|
||||||
|
|
||||||
c_char_pp = POINTER(c_char_p)
|
|
||||||
c_int_p = POINTER(c_int)
|
|
||||||
|
|
||||||
class AES_KEY(Structure):
|
|
||||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
|
||||||
('rounds', c_int)]
|
|
||||||
AES_KEY_p = POINTER(AES_KEY)
|
|
||||||
|
|
||||||
def F(restype, name, argtypes):
|
|
||||||
func = getattr(libcrypto, name)
|
|
||||||
func.restype = restype
|
|
||||||
func.argtypes = argtypes
|
|
||||||
return func
|
|
||||||
|
|
||||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
|
||||||
[c_char_p, c_int, AES_KEY_p])
|
|
||||||
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
|
|
||||||
[c_char_p, c_char_p, AES_KEY_p, c_int])
|
|
||||||
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, userkey):
|
|
||||||
self._blocksize = len(userkey)
|
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
|
||||||
raise ENCRYPTIONError(_('AES improper key used'))
|
|
||||||
return
|
|
||||||
key = self._key = AES_KEY()
|
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
|
||||||
if rv < 0:
|
|
||||||
raise ENCRYPTIONError(_('Failed to initialize AES key'))
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
clear = b''
|
|
||||||
for i in range(0, len(data), 16):
|
|
||||||
out = create_string_buffer(16)
|
|
||||||
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
|
|
||||||
if rv == 0:
|
|
||||||
raise ENCRYPTIONError(_('AES decryption failed'))
|
|
||||||
clear += out.raw
|
|
||||||
return clear
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto_pycrypto():
|
|
||||||
from Crypto.Cipher import AES as _AES
|
|
||||||
class AES(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self._aes = _AES.new(key, _AES.MODE_ECB)
|
|
||||||
|
|
||||||
def decrypt(self, data):
|
|
||||||
return self._aes.decrypt(data)
|
|
||||||
|
|
||||||
return AES
|
|
||||||
|
|
||||||
def _load_crypto():
|
|
||||||
AES = None
|
|
||||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
|
||||||
for loader in cryptolist:
|
|
||||||
try:
|
|
||||||
AES = loader()
|
|
||||||
break
|
|
||||||
except (ImportError, ENCRYPTIONError):
|
|
||||||
pass
|
|
||||||
return AES
|
|
||||||
|
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
# Wrap a stream so that output gets flushed immediately
|
||||||
# and also make sure that any unicode strings get
|
# and also make sure that any unicode strings get
|
||||||
# encoded using "replace" before writing them.
|
# encoded using "replace" before writing them.
|
||||||
@@ -286,10 +224,17 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,str):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.buffer.write(data)
|
try:
|
||||||
self.stream.buffer.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
@@ -367,11 +312,17 @@ class KoboLibrary(object):
|
|||||||
if sys.getwindowsversion().major > 5:
|
if sys.getwindowsversion().major > 5:
|
||||||
if 'LOCALAPPDATA' in os.environ.keys():
|
if 'LOCALAPPDATA' in os.environ.keys():
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
if sys.version_info[0] == 2:
|
||||||
|
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
|
||||||
|
else:
|
||||||
|
self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
||||||
if (self.kobodir == u""):
|
if (self.kobodir == u""):
|
||||||
if 'USERPROFILE' in os.environ.keys():
|
if 'USERPROFILE' in os.environ.keys():
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
|
if sys.version_info[0] == 2:
|
||||||
|
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), "Local Settings", "Application Data")
|
||||||
|
else:
|
||||||
|
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
|
||||||
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
|
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
|
||||||
elif sys.platform.startswith('darwin'):
|
elif sys.platform.startswith('darwin'):
|
||||||
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
|
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
|
||||||
@@ -424,6 +375,7 @@ class KoboLibrary(object):
|
|||||||
olddb.close()
|
olddb.close()
|
||||||
self.newdb.close()
|
self.newdb.close()
|
||||||
self.__sqlite = sqlite3.connect(self.newdb.name)
|
self.__sqlite = sqlite3.connect(self.newdb.name)
|
||||||
|
self.__sqlite.text_factory = lambda b: b.decode("utf-8", errors="ignore")
|
||||||
self.__cursor = self.__sqlite.cursor()
|
self.__cursor = self.__sqlite.cursor()
|
||||||
self._userkeys = []
|
self._userkeys = []
|
||||||
self._books = []
|
self._books = []
|
||||||
@@ -497,9 +449,15 @@ class KoboLibrary(object):
|
|||||||
for m in matches:
|
for m in matches:
|
||||||
# print "m:{0}".format(m[0])
|
# print "m:{0}".format(m[0])
|
||||||
macaddrs.append(m[0].upper())
|
macaddrs.append(m[0].upper())
|
||||||
|
elif sys.platform.startswith('linux'):
|
||||||
|
for interface in os.listdir('/sys/class/net'):
|
||||||
|
with open('/sys/class/net/' + interface + '/address', 'r') as f:
|
||||||
|
mac = f.read().strip().upper()
|
||||||
|
# some interfaces, like Tailscale's VPN interface, do not have a MAC address
|
||||||
|
if mac != '':
|
||||||
|
macaddrs.append(mac)
|
||||||
else:
|
else:
|
||||||
# probably linux
|
# final fallback
|
||||||
|
|
||||||
# let's try ip
|
# let's try ip
|
||||||
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||||
for line in os.popen('ip -br link'):
|
for line in os.popen('ip -br link'):
|
||||||
@@ -630,11 +588,9 @@ class KoboFile(object):
|
|||||||
file page key. The caller must determine if the decrypted
|
file page key. The caller must determine if the decrypted
|
||||||
data is correct."""
|
data is correct."""
|
||||||
# The userkey decrypts the page key (self.key)
|
# The userkey decrypts the page key (self.key)
|
||||||
keyenc = AES(userkey)
|
decryptedkey = AES.new(userkey, AES.MODE_ECB).decrypt(self.key)
|
||||||
decryptedkey = keyenc.decrypt(self.key)
|
# The decrypted page key decrypts the content. Padding is PKCS#7
|
||||||
# The decrypted page key decrypts the content
|
return unpad(AES.new(decryptedkey, AES.MODE_ECB).decrypt(contents), 16)
|
||||||
pageenc = AES(decryptedkey)
|
|
||||||
return self.__removeaespadding(pageenc.decrypt(contents))
|
|
||||||
|
|
||||||
def check (self, contents):
|
def check (self, contents):
|
||||||
"""
|
"""
|
||||||
@@ -704,23 +660,6 @@ class KoboFile(object):
|
|||||||
raise ValueError()
|
raise ValueError()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __removeaespadding (self, contents):
|
|
||||||
"""
|
|
||||||
Remove the trailing padding, using what appears to be the CMS
|
|
||||||
algorithm from RFC 5652 6.3"""
|
|
||||||
lastchar = binascii.b2a_hex(contents[-1:])
|
|
||||||
strlen = int(lastchar, 16)
|
|
||||||
padding = strlen
|
|
||||||
if strlen == 1:
|
|
||||||
return contents[:-1]
|
|
||||||
if strlen < 16:
|
|
||||||
for i in range(strlen):
|
|
||||||
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
|
|
||||||
if testchar != lastchar:
|
|
||||||
padding = 0
|
|
||||||
if padding > 0:
|
|
||||||
contents = contents[:-padding]
|
|
||||||
return contents
|
|
||||||
|
|
||||||
def decrypt_book(book, lib):
|
def decrypt_book(book, lib):
|
||||||
print("Converting {0}".format(book.title))
|
print("Converting {0}".format(book.title))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>Obok DeDRM Plugin</h1>
|
<h1>Obok DeDRM Plugin</h1>
|
||||||
<h3>(version 10.0.0)</h3>
|
<h3>(version 10.0.9 / 10.1.0 RC1)</h3>
|
||||||
|
|
||||||
<h3>Installation:</h3>
|
<h3>Installation:</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -224,5 +224,5 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
|||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
if text is None:
|
if text is None:
|
||||||
text = ''
|
text = ''
|
||||||
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
|
||||||
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|||||||
@@ -68,13 +68,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -39,13 +39,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -45,13 +45,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -56,13 +56,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -48,13 +48,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -51,13 +51,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
except:
|
except:
|
||||||
@@ -292,7 +300,7 @@ if iswindows:
|
|||||||
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
|
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
|
||||||
if more == None: # no more calls to decrypt, should have all the data
|
if more == None: # no more calls to decrypt, should have all the data
|
||||||
if numExtraBytes != 0:
|
if numExtraBytes != 0:
|
||||||
raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
|
raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
|
||||||
|
|
||||||
# hold back some bytes in case last decrypt has zero len
|
# hold back some bytes in case last decrypt has zero len
|
||||||
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
|
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
|
||||||
@@ -334,7 +342,7 @@ if iswindows:
|
|||||||
def removePad(self, paddedBinaryString, blockSize):
|
def removePad(self, paddedBinaryString, blockSize):
|
||||||
""" Remove padding from a binary string """
|
""" Remove padding from a binary string """
|
||||||
if not(0<len(paddedBinaryString)):
|
if not(0<len(paddedBinaryString)):
|
||||||
raise DecryptNotBlockAlignedError, 'Expected More Data'
|
raise DecryptNotBlockAlignedError('Expected More Data')
|
||||||
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
|
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
|
||||||
|
|
||||||
class noPadding(Pad):
|
class noPadding(Pad):
|
||||||
@@ -364,8 +372,8 @@ if iswindows:
|
|||||||
self.blockSize = blockSize # blockSize is in bytes
|
self.blockSize = blockSize # blockSize is in bytes
|
||||||
self.padding = padding # change default to noPadding() to get normal ECB behavior
|
self.padding = padding # change default to noPadding() to get normal ECB behavior
|
||||||
|
|
||||||
assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
|
assert( keySize%4==0 and keySize/4 in NrTable[4]),'key size must be 16,20,24,29 or 32 bytes'
|
||||||
assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
|
assert( blockSize%4==0 and blockSize/4 in NrTable), 'block size must be 16,20,24,29 or 32 bytes'
|
||||||
|
|
||||||
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
|
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
|
||||||
self.Nk = keySize/4 # Nk is the key length in 32-bit words
|
self.Nk = keySize/4 # Nk is the key length in 32-bit words
|
||||||
@@ -642,7 +650,7 @@ if iswindows:
|
|||||||
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
|
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
|
||||||
""" Initialize AES, keySize is in bytes """
|
""" Initialize AES, keySize is in bytes """
|
||||||
if not (keySize == 16 or keySize == 24 or keySize == 32) :
|
if not (keySize == 16 or keySize == 24 or keySize == 32) :
|
||||||
raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
|
raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
|
||||||
|
|
||||||
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
|
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
|
||||||
|
|
||||||
@@ -782,10 +790,11 @@ if iswindows:
|
|||||||
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||||
def pbkdf2(self, passwd, salt, iter, keylen):
|
def pbkdf2(self, passwd, salt, iter, keylen):
|
||||||
|
|
||||||
def xorstr( a, b ):
|
def xorbytes( a, b ):
|
||||||
if len(a) != len(b):
|
if len(a) != len(b):
|
||||||
raise Exception("xorstr(): lengths differ")
|
raise Exception("xorbytes(): lengths differ")
|
||||||
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
|
return bytes([x ^ y for x, y in zip(a, b)])
|
||||||
|
|
||||||
|
|
||||||
def prf( h, data ):
|
def prf( h, data ):
|
||||||
hm = h.copy()
|
hm = h.copy()
|
||||||
@@ -797,13 +806,13 @@ if iswindows:
|
|||||||
T = U
|
T = U
|
||||||
for i in range(2, itercount+1):
|
for i in range(2, itercount+1):
|
||||||
U = prf( h, U )
|
U = prf( h, U )
|
||||||
T = xorstr( T, U )
|
T = xorbytes( T, U )
|
||||||
return T
|
return T
|
||||||
|
|
||||||
sha = hashlib.sha1
|
sha = hashlib.sha1
|
||||||
digest_size = sha().digest_size
|
digest_size = sha().digest_size
|
||||||
# l - number of output blocks to produce
|
# l - number of output blocks to produce
|
||||||
l = keylen / digest_size
|
l = keylen // digest_size
|
||||||
if keylen % digest_size != 0:
|
if keylen % digest_size != 0:
|
||||||
l += 1
|
l += 1
|
||||||
h = hmac.new( passwd, None, sha )
|
h = hmac.new( passwd, None, sha )
|
||||||
|
|||||||
@@ -32,13 +32,21 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
iswindows = sys.platform.startswith('win')
|
iswindows = sys.platform.startswith('win')
|
||||||
isosx = sys.platform.startswith('darwin')
|
isosx = sys.platform.startswith('darwin')
|
||||||
|
|
||||||
|
|||||||
@@ -276,14 +276,22 @@ class SafeUnbuffered:
|
|||||||
if self.encoding == None:
|
if self.encoding == None:
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
if isinstance(data,unicode):
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
data = data.encode(self.encoding,"replace")
|
data = data.encode(self.encoding,"replace")
|
||||||
self.stream.write(data)
|
try:
|
||||||
self.stream.flush()
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class KoboLibrary(object):
|
class KoboLibrary(object):
|
||||||
"""The Kobo library.
|
"""The Kobo library.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
14
README.md
14
README.md
@@ -1,18 +1,22 @@
|
|||||||
# DeDRM_tools
|
# DeDRM_tools
|
||||||
DeDRM tools for ebooks
|
DeDRM tools for ebooks
|
||||||
|
|
||||||
This is a fork of Apprentice Harper's version of the DeDRM tools. I've added some of the PRs that still haven't been merged, as well as added some more features / bugfixes myself.
|
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.
|
||||||
|
|
||||||
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). This plugin will start with version v10.0.0 so there won't be conflicting / duplicate version numbers when Apprentice Harper's version is updated again.
|
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).
|
||||||
|
|
||||||
The v10.0.0 versions of this plugin should both work with Calibre 5.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.
|
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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
# Original README from Apprentice Harper
|
# Original README from Apprentice Harper
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
The individual scripts are now released as two plugins for calibre: DeDRM and Obok.
|
The individual scripts are now released as two plugins for calibre: DeDRM and Obok.
|
||||||
The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
|
The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM, Barnes & Noble DRM, and some historical formats.
|
||||||
The Obok plugin handles Kobo DRM.
|
The Obok plugin handles Kobo DRM.
|
||||||
|
|
||||||
Users with calibre 5.x or later should use release 7.2.0 or later of the tools.
|
Users with calibre 5.x or later should use release 7.2.0 or later of the tools.
|
||||||
@@ -24,7 +28,7 @@ Note that Amazon changes the DRM for KFX files frequently. What works for KFX to
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
I urge people to read the FAQs. But to cover the most common: Use ADE 2.0.1 to be sure not to get the new DRM scheme that these tools can't handle. 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.
|
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.
|
||||||
|
|
||||||
My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools.
|
My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
Welcome to the tools!
|
Welcome to the tools!
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
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 Apprentice Harper's github repository: https://github.com/apprenticeharper/DeDRM_tools/
|
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/
|
||||||
|
|
||||||
This archive includes calibre plugins to remove DRM from:
|
This archive includes calibre plugins to remove DRM from:
|
||||||
|
|
||||||
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
|
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
|
||||||
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
- Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||||
- Adobe Digital Editions (v2.0.1) PDFs
|
- Adobe Digital Editions PDFs
|
||||||
- Kobo kePubs from the Kobo Desktop application and attached Kobo readers.
|
- Kobo kePubs from the Kobo Desktop application and attached Kobo readers.
|
||||||
|
|
||||||
These tools do NOT work with Apple's iBooks FairPlay DRM. Use iBook Copy from TunesKit.
|
These tools do NOT work with Apple's iBooks FairPlay DRM. Use iBook Copy from TunesKit.
|
||||||
These tools no longer work well with books from Barnes & Noble.
|
These tools no longer work well with books from Barnes & Noble.
|
||||||
|
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.
|
||||||
|
|
||||||
For limitations and work-arounds, see the FAQ at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md
|
For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md
|
||||||
|
|
||||||
About the tools
|
About the tools
|
||||||
---------------
|
---------------
|
||||||
These tools are updated and maintained by Apprentice Harper and many others. You can find the latest updates at Apprentice Harper's github repository https://github.com/apprenticeharper/DeDRM_tools/ and get support by creating an issue at the repository (github account required) or by posting a comment at Apprentice Alf's blog: http://www.apprenticealf.wordpress.com/
|
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).
|
||||||
|
|
||||||
If you re-post these tools, a link to the repository and/or the blog would be appreciated.
|
If you re-post these tools, a link to the repository would be appreciated.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
DeDRM plugin for calibre (Mac OS X, Windows)
|
DeDRM plugin for calibre (Linux, Mac OS X and Windows)
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. If you have calibre 5, you must use version 7.x or later of the plugins. For calibre 4.x and earlier, use version 6.8.x of the plugins.
|
calibe 5.x and later are now written in Python 3, and plugins must also use Python 3.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -40,18 +41,6 @@ To import ebooks from the Kobo Desktop app or from a Kobo ebook reader, install
|
|||||||
For instructions, see the obok_plugin_ReadMe.txt file.
|
For instructions, see the obok_plugin_ReadMe.txt file.
|
||||||
|
|
||||||
|
|
||||||
DeDRM application for Mac OS X users: (Mac OS X 10.6 and above)
|
|
||||||
---------------------------------------------------------------
|
|
||||||
DeDRM application for Windows users: (Windows XP through Windows 10)
|
|
||||||
------------------------------------------------------------------
|
|
||||||
As of Version 6.7 of the tools, these are no longer provided or supported.
|
|
||||||
|
|
||||||
|
|
||||||
Linux support
|
|
||||||
-------------
|
|
||||||
It may be possible to use the plugins on a Linux system, but no support is given at this time.
|
|
||||||
|
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
-------
|
-------
|
||||||
The original inept and ignoble scripts were by i♥cabbages
|
The original inept and ignoble scripts were by i♥cabbages
|
||||||
@@ -61,7 +50,8 @@ The original topaz DRM removal script was by CMBDTC
|
|||||||
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
|
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
|
||||||
The original KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997
|
The original KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997
|
||||||
The alfcrypto library is by some_updates
|
The alfcrypto library is by some_updates
|
||||||
The DeDRM plugin was based on plugins by DiapDealer and is maintained by Apprentice Alf and Apprentice Harper
|
The DeDRM plugin is based on plugins by DiapDealer and is currently maintained by noDRM
|
||||||
|
The DeDRM plugin has been maintained by Apprentice Alf and Apprentice Harper until 2021.
|
||||||
|
|
||||||
The original obok script was by Physisticated
|
The original obok script was by Physisticated
|
||||||
The plugin conversion was done anonymously.
|
The plugin conversion was done anonymously.
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Installation
|
|||||||
------------
|
------------
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
Customization
|
Customization
|
||||||
-------------
|
-------------
|
||||||
@@ -16,7 +18,6 @@ No customization is required, except choosing which menus will show the plugin.
|
|||||||
|
|
||||||
Using the plugin
|
Using the plugin
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
@@ -29,5 +30,5 @@ If you find that the DeDRM plugin is not working for you (imported ebooks still
|
|||||||
- Once calibre has re-started, import the problem ebook.
|
- Once calibre has re-started, import the problem ebook.
|
||||||
- Now close calibre.
|
- Now close calibre.
|
||||||
|
|
||||||
A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file.
|
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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user