mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 13:08:55 +00:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44b50d1d8 | ||
|
|
05e0d0bedb | ||
|
|
1b391da815 | ||
|
|
1545d76803 | ||
|
|
d9353bdd93 | ||
|
|
5d10420422 | ||
|
|
f20bede242 | ||
|
|
39f8595139 | ||
|
|
9c41716e5e | ||
|
|
b4c0e33b8b | ||
|
|
90910ab106 | ||
|
|
88dd1350c0 | ||
|
|
40a8e4360b | ||
|
|
17ccc4d1b9 | ||
|
|
30425c1ec8 | ||
|
|
77dcc462aa | ||
|
|
be57bcca7d | ||
|
|
4a58f7017c | ||
|
|
eae512da8c | ||
|
|
7058fbeb98 | ||
|
|
8cd3523a17 | ||
|
|
cc17d9cc59 | ||
|
|
969fe52e13 | ||
|
|
95fc924d1a | ||
|
|
0313088c15 | ||
|
|
066e613cee | ||
|
|
14947cd10c | ||
|
|
0005bba3c3 | ||
|
|
8e10b090a2 | ||
|
|
007a8e8a15 | ||
|
|
73af5d355d | ||
|
|
45a1a64db5 | ||
|
|
bc1c3c2197 | ||
|
|
79cfddfbee | ||
|
|
aa41bba68c | ||
|
|
86a90117e5 | ||
|
|
874a6b8de9 | ||
|
|
01c654cb68 | ||
|
|
5bc28623cb | ||
|
|
c1d7fcbb7f | ||
|
|
45eefd6c80 | ||
|
|
33e37eb375 | ||
|
|
4229b8ff85 | ||
|
|
91e4645315 | ||
|
|
425d8af73e | ||
|
|
0ce86fa8db | ||
|
|
ecc7db09a9 | ||
|
|
d7ddc2ab93 | ||
|
|
fd51422a36 | ||
|
|
cb36ca1b0d | ||
|
|
76a47e0dd0 | ||
|
|
70a754fb46 | ||
|
|
ffd79d5fe4 | ||
|
|
21a7b13524 | ||
|
|
52bdbe95c9 | ||
|
|
495dda3809 | ||
|
|
52e83922c0 | ||
|
|
6cbc5285cb | ||
|
|
33b9630ca5 | ||
|
|
9346f86f73 | ||
|
|
8d2d6627cf | ||
|
|
6f198b247c | ||
|
|
9fb95eff41 | ||
|
|
0b2b81fd23 | ||
|
|
63aecc598f | ||
|
|
51c8be6baf | ||
|
|
7aab8a3711 | ||
|
|
2789cee331 | ||
|
|
823704cf36 | ||
|
|
a7974f0f14 | ||
|
|
ed412bee35 | ||
|
|
6cee615f26 | ||
|
|
c4581b4d72 | ||
|
|
f6a568bcc1 | ||
|
|
bf6170e613 | ||
|
|
afcd79c0cc | ||
|
|
fdf0389936 | ||
|
|
5599c1694b | ||
|
|
dff90fae6f | ||
|
|
d33f679eae | ||
|
|
225e74a334 | ||
|
|
13e9a14907 | ||
|
|
92ea0a2f24 | ||
|
|
a1059650f6 | ||
|
|
a3cc221932 | ||
|
|
6732be1434 | ||
|
|
ad5cb056f0 | ||
|
|
d3c7388327 | ||
|
|
8e436ad920 | ||
|
|
ae806f734e | ||
|
|
ccfa454226 | ||
|
|
6716db1f62 | ||
|
|
0e0d7d8b14 | ||
|
|
981aadc497 | ||
|
|
26eb5d676c | ||
|
|
464788a3f1 | ||
|
|
036f9007fd | ||
|
|
bdd1c2e474 | ||
|
|
54a58d05a5 | ||
|
|
218539f131 | ||
|
|
f9d9b6016f | ||
|
|
131cea1215 | ||
|
|
731eeac087 | ||
|
|
cdab22e59c | ||
|
|
b8b324956c | ||
|
|
1955b34883 | ||
|
|
dbc5c2b4de | ||
|
|
856fef55be | ||
|
|
f2fa0426b7 | ||
|
|
c3376cc492 | ||
|
|
dc72c368a5 | ||
|
|
77033e1602 | ||
|
|
15cd372ad9 | ||
|
|
c52e4db3df | ||
|
|
45038cc77b | ||
|
|
5ec9c98a0b | ||
|
|
66bab7bd7d | ||
|
|
e0c7d7d382 | ||
|
|
971db9ae71 | ||
|
|
cf829db532 | ||
|
|
80c8bd2d24 | ||
|
|
969599ce6b |
33
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
33
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Question
|
||||
about: Questions for DeDRM Project
|
||||
title: "[QUESTION] Title"
|
||||
labels: Question
|
||||
---
|
||||
|
||||
## CheckList
|
||||
<!-- Check with `[x]` -->
|
||||
- [ ] `The Title` and The `Log Title` are setted correctly.
|
||||
- [ ] Clarified about `my environment`.
|
||||
- [ ] Code block is used for `the log`.
|
||||
<!-- If you don't know the version, please specify 'Unknown'. -->
|
||||
<!-- In case of markdown To use the code block, enclose it in ```. -->
|
||||
<!-- If you don't need Log, please delete the log section. -->
|
||||
|
||||
---
|
||||
|
||||
## Title
|
||||
<!-- content -->
|
||||
|
||||
## My Environment
|
||||
### Calibre: `Version`
|
||||
### Kindle: `Version`
|
||||
### DeDRM: `Version`
|
||||
|
||||
## Log
|
||||
<details><summary>Log Title</summary>
|
||||
|
||||
```log
|
||||
PUT YOUR LOG
|
||||
```
|
||||
</details>
|
||||
41
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Question
|
||||
description: Questions for DeDRM Project
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question / bug report
|
||||
description: Please enter your question / your bug report.
|
||||
- type: input
|
||||
id: calibre-version
|
||||
attributes:
|
||||
label: Which version of Calibre are you running?
|
||||
description: "Example: 5.32"
|
||||
placeholder: "5.32"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Which version of the DeDRM plugin are you running?
|
||||
description: "Example: v10.0.0"
|
||||
placeholder: "v10.0.0"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: kindle-version
|
||||
attributes:
|
||||
label: If applicable, which version of the Kindle software are you running?
|
||||
description: "Example: 1.24"
|
||||
placeholder: "Leave empty if unrelated to Kindle books"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log output
|
||||
description: If applicable, please post your log output here - into the code block.
|
||||
value: |
|
||||
```log
|
||||
Paste log output here.
|
||||
```
|
||||
35
.github/workflows/Python_tests.yml
vendored
35
.github/workflows/Python_tests.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Python_tests
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
Python_tests:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
python-version: [2.7, 3.8]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest # -r requirements.txt
|
||||
#- name: Check formatting with black
|
||||
# if: matrix.python-version == '3.8'
|
||||
# run: |
|
||||
# pip install black
|
||||
# black --check .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --builtins=_,I --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --builtins=_,I --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
#- name: Test with pytest
|
||||
# run: pytest
|
||||
#- name: Run doctests with pytest
|
||||
# run: pytest --doctest-modules
|
||||
19
.github/workflows/main.yml
vendored
Normal file
19
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Package plugin
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Package
|
||||
run: python3 make_release.py 10.0.0
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: plugin
|
||||
path: |
|
||||
DeDRM_tools_*.zip
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
23
CHANGELOG.md
Normal file
23
CHANGELOG.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
List of changes since the fork of Apprentice Harper's repository:
|
||||
|
||||
- CI testing / linting removed as that always failed anyways. The CI now "just" packages the plugin.
|
||||
- Support for the Readium LCP DRM (also known as "CARE DRM" or "TEA DRM"). This supports EPUB and PDF files. It does not yet support Readium LCPDF/LPF/LCPA/LCPAU/LCPDI files, as I don't have access to any of these. If you have an LCP-protected file in one of these formats that this plugin does not work with, please open [an issue](https://github.com/noDRM/DeDRM_tools/issues) and attach the file to the report.
|
||||
- Add new Github issue report form which forces the user to include stuff like their Calibre version to hopefully increase the quality of bug reports.
|
||||
- Issues with PDF files in Calibre 5 should be fixed (merged [apprenticeharper/DeDRM_tools#1689](https://github.com/apprenticeharper/DeDRM_tools/pull/1689) ).
|
||||
- Fixed tons of issues with the B&N PDF DRM removal script ignoblepdf.py. It looks like that has never been tested since the move to Python3. I have integrated the B&N-specific code into ineptpdf.py, the original ignoblepdf.py is now unused. Fairly untested as I don't have any PDFs with B&N DRM.
|
||||
- Issues with Obok key retrieval fixed (merged [apprenticeharper/DeDRM_tools#1691](https://github.com/apprenticeharper/DeDRM_tools/pull/1691) ).
|
||||
- Issues with obfuscated Adobe fonts fixed (fixes [apprenticeharper/DeDRM_tools#1828](https://github.com/apprenticeharper/DeDRM_tools/issues/1828) ).
|
||||
- Deobfuscate font files in EPUBs by default (can be disabled in the plugin settings).
|
||||
- The standalone adobekey.py script now includes the account UUID in the key file name.
|
||||
- When extracting the default key from an ADE install, include the account UUID in the key name.
|
||||
- Adobe key management window size increased to account for longer key names due to the UUID.
|
||||
- Verify that the decrypted book key has the correct format. This makes it way less likely for issue [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862) to cause trouble.
|
||||
- If the Adobe owner UUID of a book being imported happens to be included in a particular key's name, try this key first before trying all the others. This completely fixes [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862), but only if the key name contains the correct UUID (not always the case, especially for keys imported with older versions of the plugin). It also makes DRM removal faster as the plugin no longer has to attempt all possible keys.
|
||||
- Remove some additional DRM remnants in Amazon MOBI files (merged [apprenticeharper/DeDRM_tools#23](https://github.com/apprenticeharper/DeDRM_tools/pull/23) ).
|
||||
- Just in case it's necessary, added a setting to the B&N key generation script to optionally allow the user to select the old key generation algorithm. Who knows, they might want to remove DRM from old books with the old key scheme.
|
||||
- Add a more verbose error message when trying to remove DRM from a book with the new, not-yet-cracked version of the Adobe ADEPT DRM.
|
||||
- Added back support for Python2 (Calibre 2.0+). Only tested with ADEPT (PDF & EPUB) and Readium LCP so far, please open an issue if there's errors with other book types.
|
||||
- Begin work on removing some kinds of watermarks from files after DRM removal. This isn't tested a lot, and is disabled by default. You can enable it in the plugin settings.
|
||||
- If you're using the [ACSM Input Plugin / DeACSM](https://www.mobileread.com/forums/showthread.php?t=341975), the encryption key will automatically be extracted from that plugin if necessary.
|
||||
@@ -38,7 +38,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<h3>Renaming 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 sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<h3>Renaming 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 sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
@@ -56,7 +56,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
||||
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
<h3>NOOK Study</h3>
|
||||
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
|
||||
|
||||
@@ -36,7 +36,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@@ -17,15 +17,19 @@ p {margin-top: 0}
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v6.7.0)</span></h1>
|
||||
<h1>DeDRM Plugin <span class="version">(v10.0.0)</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>It is a forked version created by NoDRM, based on the original plugin by Apprentice Alf and Apprentice Harper.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<p>If you are using the <a href="https://www.mobileread.com/forums/showthread.php?t=341975">DeACSM / ACSM Input Plugin</a> for Calibre, the keys will also automatically be dumped for you.</p>
|
||||
|
||||
<p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p>
|
||||
|
||||
@@ -42,11 +46,12 @@ p {margin-top: 0}
|
||||
|
||||
<h3>Credits:</h3>
|
||||
<ul>
|
||||
<li>NoDRM for a bunch of updates and the Readium LCP support</li>
|
||||
<li>The Dark Reverser for the Mobipocket and eReader 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>CMBDTC for Amazon Topaz DRM removal script</li>
|
||||
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
|
||||
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
|
||||
<li>DiapDealer for the first calibre plugin versions of the tools</li>
|
||||
<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
|
||||
<li>some_updates for the DeDRM all-in-one Python tool</li>
|
||||
@@ -55,7 +60,8 @@ p {margin-top: 0}
|
||||
<li>And probably many more.</li>
|
||||
</ul>
|
||||
|
||||
<h3> For additional help read the <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 ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
|
||||
<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>
|
||||
|
||||
|
||||
<h2>Linux Systems Only</h2>
|
||||
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
|
||||
|
||||
@@ -38,7 +38,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<h3>Renaming 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 sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</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 sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
@@ -46,7 +46,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<h3>Linux Users: WINEPREFIX</h3>
|
||||
|
||||
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
|
||||
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are using the Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
41
DeDRM_plugin/DeDRM_Readium LCP passphrase_Help.htm
Normal file
41
DeDRM_plugin/DeDRM_Readium LCP passphrase_Help.htm
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Managing Readium LCP passphrases</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing Readium LCP passphrases</h1>
|
||||
|
||||
<p>Readium LCP is a relatively new eBook DRM. It's also known under the names "CARE DRM" or "TEA DRM". It does not rely on any accounts or key data that's difficult to acquire. All you need to open (or decrypt) LCP eBooks is the account passphrase given to you by the eBook provider - the very same passphrase you'd have to enter into your eBook reader device (once) to read LCP-encrypted books.</p>
|
||||
|
||||
|
||||
<h3>Entering an LCP passphrase:</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 for entering a new passphrase.</p>
|
||||
|
||||
<p>Just enter your passphrase as provided with the book, then click the OK button to save the passphrase. </p>
|
||||
|
||||
<p>Usually, passphrases are identical for all books bought with the same account. So if you buy multiple LCP-protected eBooks, they'll usually all have the same passphrase if they've all been bought at the same store with the same account. </p>
|
||||
|
||||
<h3>Deleting an LCP passphrase:</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 red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '7.0.0'
|
||||
__version__ = '10.0.0'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
@@ -69,24 +70,31 @@ __docformat__ = 'restructuredtext en'
|
||||
# 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018
|
||||
# 6.7.0 - Handle new library in calibre.
|
||||
# 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+)
|
||||
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who comtibuted
|
||||
# 6.8.1 - Kindle key fix for Mac OS X Big Sur
|
||||
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who contributed
|
||||
# 7.0.1 - More Python 3 changes. Adobe PDF decryption should now work in some cases
|
||||
# 7.0.2 - More Python 3 changes. Adobe PDF decryption should now work on PC too.
|
||||
# 7.0.3 - More Python 3 changes. Integer division in ineptpdf.py
|
||||
# 7.1.0 - Full release for calibre 5.x
|
||||
# 7.2.0 - Update for latest KFX changes, and Python 3 Obok fixes.
|
||||
# 7.2.1 - Whitespace!
|
||||
# 10.0.0 - First forked version by NoDRM. See CHANGELOG.md for details.
|
||||
|
||||
"""
|
||||
Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = (7, 0, 0)
|
||||
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
|
||||
import codecs
|
||||
import sys, os, re
|
||||
import sys, os
|
||||
import time
|
||||
import zipfile
|
||||
import traceback
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
class DeDRMError(Exception):
|
||||
pass
|
||||
@@ -107,24 +115,28 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
try:
|
||||
self.stream.buffer.write(data)
|
||||
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
|
||||
pass
|
||||
raise
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
class DeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
|
||||
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."
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = "Apprentice Alf, Aprentice Harper, The Dark Reverser and i♥cabbages"
|
||||
author = "Apprentice Alf, Apprentice Harper, NoDRM, The Dark Reverser and i♥cabbages"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (5, 0, 0) # Python 3.
|
||||
#minimum_calibre_version = (5, 0, 0) # Python 3.
|
||||
minimum_calibre_version = (2, 0, 0) # Needs Calibre 1.0 minimum. 1.X untested.
|
||||
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip'])
|
||||
on_import = True
|
||||
on_preprocess = True
|
||||
@@ -143,6 +155,7 @@ class DeDRM(FileTypePlugin):
|
||||
The extraction only happens once per version of the plugin
|
||||
Also perform upgrade of preferences once per version
|
||||
"""
|
||||
|
||||
try:
|
||||
self.pluginsdir = os.path.join(config_dir,"plugins")
|
||||
if not os.path.exists(self.pluginsdir):
|
||||
@@ -192,10 +205,64 @@ class DeDRM(FileTypePlugin):
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
def postProcessEPUB(self, path_to_ebook):
|
||||
# This is called after the DRM is removed (or if no DRM was present)
|
||||
# It does stuff like de-obfuscating fonts (by calling checkFonts)
|
||||
# or removing watermarks.
|
||||
|
||||
try:
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
if dedrmprefs["deobfuscate_fonts"] is True:
|
||||
# Deobfuscate fonts
|
||||
path_to_ebook = self.checkFonts(path_to_ebook) or path_to_ebook
|
||||
|
||||
if dedrmprefs["remove_watermarks"] is True:
|
||||
import calibre_plugins.dedrm.epubwatermark as watermark
|
||||
|
||||
# Remove Tolino's CDP watermark file
|
||||
path_to_ebook = watermark.removeCDPwatermark(self, path_to_ebook) or path_to_ebook
|
||||
|
||||
# Remove watermarks (currently just Amazon) from the OPF file
|
||||
path_to_ebook = watermark.removeOPFwatermarks(self, path_to_ebook) or path_to_ebook
|
||||
|
||||
# Remove watermarks (currently just Adobe's resource ID) from all HTML and XHTML files
|
||||
path_to_ebook = watermark.removeHTMLwatermarks(self, path_to_ebook) or path_to_ebook
|
||||
|
||||
return path_to_ebook
|
||||
|
||||
except:
|
||||
print("Error while checking settings")
|
||||
return path_to_ebook
|
||||
|
||||
def checkFonts(self, path_to_ebook):
|
||||
# This is called after the normal DRM removal is done.
|
||||
# It checks if there's fonts that need to be deobfuscated
|
||||
|
||||
try:
|
||||
import calibre_plugins.dedrm.epubfontdecrypt as epubfontdecrypt
|
||||
|
||||
output = self.temporary_file(".epub").name
|
||||
ret = epubfontdecrypt.decryptFontsBook(path_to_ebook, output)
|
||||
|
||||
if (ret == 0):
|
||||
return output
|
||||
elif (ret == 1):
|
||||
return path_to_ebook
|
||||
else:
|
||||
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
raise DeDRMError("Font deobfuscation failed")
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
def ePubDecrypt(self,path_to_ebook):
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
import calibre_plugins.dedrm.zipfix
|
||||
import calibre_plugins.dedrm.zipfix as zipfix
|
||||
|
||||
inf = self.temporary_file(".epub")
|
||||
try:
|
||||
@@ -204,12 +271,28 @@ class DeDRM(FileTypePlugin):
|
||||
fr.fix()
|
||||
except Exception as e:
|
||||
print("{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
|
||||
raise Exception(e)
|
||||
raise
|
||||
|
||||
# import the decryption keys
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
|
||||
# import the LCP handler
|
||||
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||
|
||||
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
||||
try:
|
||||
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
|
||||
except:
|
||||
print("Looks like that didn't work:")
|
||||
raise
|
||||
|
||||
return self.postProcessEPUB(retval)
|
||||
|
||||
|
||||
# Not an LCP book, do the normal EPUB (Adobe) handling.
|
||||
|
||||
# import the Barnes & Noble ePub handler
|
||||
import calibre_plugins.dedrm.ignobleepub as ignobleepub
|
||||
|
||||
@@ -237,7 +320,7 @@ class DeDRM(FileTypePlugin):
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
|
||||
|
||||
@@ -296,7 +379,7 @@ class DeDRM(FileTypePlugin):
|
||||
print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except Exception as e:
|
||||
@@ -309,7 +392,45 @@ class DeDRM(FileTypePlugin):
|
||||
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||
|
||||
if ineptepub.adeptBook(inf.name):
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
book_uuid = None
|
||||
try:
|
||||
# This tries to figure out which Adobe account UUID the book is licensed for.
|
||||
# If we know that we can directly use the correct key instead of having to
|
||||
# try them all.
|
||||
book_uuid = ineptepub.adeptGetUserUUID(inf.name)
|
||||
except:
|
||||
pass
|
||||
|
||||
if book_uuid is None:
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
else:
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||
|
||||
|
||||
if book_uuid is not None:
|
||||
# Check if we have a key with that UUID in its name:
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
if not book_uuid.lower() in keyname.lower():
|
||||
continue
|
||||
|
||||
# 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))
|
||||
of = self.temporary_file(".epub")
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
of.close()
|
||||
if result == 0:
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return self.postProcessEPUB(of.name)
|
||||
except ineptepub.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return self.postProcessEPUB(path_to_ebook)
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
@@ -320,6 +441,9 @@ class DeDRM(FileTypePlugin):
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except ineptepub.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return self.postProcessEPUB(path_to_ebook)
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
@@ -334,7 +458,7 @@ class DeDRM(FileTypePlugin):
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return of.name
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
|
||||
@@ -348,23 +472,45 @@ class DeDRM(FileTypePlugin):
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
|
||||
defaultkeys = adeptkeys()
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
try:
|
||||
self.default_key = defaultkeys[0]
|
||||
except:
|
||||
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
self.default_key = ""
|
||||
|
||||
newkeys = []
|
||||
newnames = []
|
||||
idx = 0
|
||||
for keyvalue in defaultkeys:
|
||||
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||
newkeys.append(keyvalue)
|
||||
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||
idx += 1
|
||||
|
||||
# Check for DeACSM keys:
|
||||
try:
|
||||
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||
|
||||
newkey, newname = checkForDeACSMkeys()
|
||||
|
||||
if newkey is not None:
|
||||
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))
|
||||
newkeys.append(newkey)
|
||||
newnames.append(newname)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
@@ -387,7 +533,7 @@ class DeDRM(FileTypePlugin):
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',codecs.encode(keyvalue, 'hex').decode('ascii'))
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
@@ -395,7 +541,7 @@ class DeDRM(FileTypePlugin):
|
||||
traceback.print_exc()
|
||||
print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return of.name
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except Exception as e:
|
||||
@@ -408,25 +554,81 @@ class DeDRM(FileTypePlugin):
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
# Not a Barnes & Noble nor an Adobe Adept
|
||||
# Import the fixed epub.
|
||||
# Probably a DRM-free EPUB, but we should still check for fonts.
|
||||
print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
return self.postProcessEPUB(inf.name)
|
||||
#raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
def PDFDecrypt(self,path_to_ebook):
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.ineptpdf
|
||||
|
||||
import calibre_plugins.dedrm.ineptpdf as ineptpdf
|
||||
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
|
||||
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
||||
try:
|
||||
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
|
||||
except:
|
||||
print("Looks like that didn't work:")
|
||||
raise
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
# Not an LCP book, do the normal Adobe handling.
|
||||
|
||||
book_uuid = None
|
||||
try:
|
||||
# Try to figure out which Adobe account this book is licensed for.
|
||||
book_uuid = ineptpdf.adeptGetUserUUID(path_to_ebook)
|
||||
except:
|
||||
pass
|
||||
|
||||
if book_uuid is None:
|
||||
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
else:
|
||||
print("{0} v{1}: {2} is a PDF ebook for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||
|
||||
if book_uuid is not None:
|
||||
# Check if we have a key for that UUID
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
if not book_uuid.lower() in keyname.lower():
|
||||
continue
|
||||
|
||||
# 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))
|
||||
of = self.temporary_file(".pdf")
|
||||
|
||||
try:
|
||||
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||
of.close()
|
||||
if result == 0:
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return of.name
|
||||
|
||||
except ineptpdf.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return path_to_ebook
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# If we end up here, we didn't find a key with a matching UUID, so lets just try all of them.
|
||||
|
||||
# Attempt to decrypt PDF with each encryption key (generated or provided).
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
userkey = userkeyhex.decode('hex')
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
userkey = codecs.decode(userkeyhex,'hex')
|
||||
print("{0} v{1}: Trying encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".pdf")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||
except ineptpdf.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return path_to_ebook
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
@@ -437,6 +639,7 @@ class DeDRM(FileTypePlugin):
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return of.name
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
@@ -451,23 +654,44 @@ class DeDRM(FileTypePlugin):
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
|
||||
defaultkeys = adeptkeys()
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
try:
|
||||
self.default_key = defaultkeys[0]
|
||||
except:
|
||||
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
self.default_key = ""
|
||||
|
||||
newkeys = []
|
||||
newnames = []
|
||||
idx = 0
|
||||
for keyvalue in defaultkeys:
|
||||
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
|
||||
if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values():
|
||||
newkeys.append(keyvalue)
|
||||
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||
idx += 1
|
||||
|
||||
# Check for DeACSM keys:
|
||||
try:
|
||||
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||
|
||||
newkey, newname = checkForDeACSMkeys()
|
||||
|
||||
if newkey is not None:
|
||||
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))
|
||||
newkeys.append(keyvalue)
|
||||
newnames.append(newname)
|
||||
except:
|
||||
pass
|
||||
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
@@ -490,7 +714,7 @@ class DeDRM(FileTypePlugin):
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey,'hex').decode('ascii'))
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
@@ -503,6 +727,36 @@ class DeDRM(FileTypePlugin):
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
# Unable to decrypt the PDF with any of the existing keys. Is it a B&N PDF?
|
||||
# Attempt to decrypt PDF with each encryption key (generated or provided).
|
||||
for keyname, userkey in dedrmprefs['bandnkeys'].items():
|
||||
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||
of = self.temporary_file(".pdf")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name, False)
|
||||
except ineptpdf.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return path_to_ebook
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return of.name
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
@@ -595,7 +849,7 @@ class DeDRM(FileTypePlugin):
|
||||
of = self.temporary_file(".pmlz")
|
||||
|
||||
# Give the userkey, ebook and TemporaryPersistent file to the decryption function.
|
||||
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex'))
|
||||
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, codecs.decode(userkey,'hex'))
|
||||
|
||||
of.close()
|
||||
|
||||
|
||||
@@ -51,11 +51,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -99,7 +105,7 @@ def unicode_argv():
|
||||
return ["adobekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
@@ -111,11 +117,16 @@ if iswindows:
|
||||
c_long, c_ulong
|
||||
|
||||
from ctypes.wintypes import LPVOID, DWORD, BOOL
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes.util import find_library
|
||||
libcrypto = find_library('libeay32')
|
||||
libcrypto = find_library('libcrypto-1_1')
|
||||
if libcrypto is None:
|
||||
libcrypto = find_library('libeay32')
|
||||
if libcrypto is None:
|
||||
raise ADEPTError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
@@ -360,6 +371,7 @@ if iswindows:
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
names = []
|
||||
try:
|
||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||
except WindowsError:
|
||||
@@ -372,12 +384,15 @@ if iswindows:
|
||||
ktype = winreg.QueryValueEx(plkparent, None)[0]
|
||||
if ktype != 'credentials':
|
||||
continue
|
||||
uuid_name = "Unknown"
|
||||
for j in range(0, 16):
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
if ktype == 'user':
|
||||
uuid_name = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
if ktype != 'privateLicenseKey':
|
||||
continue
|
||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
@@ -385,12 +400,13 @@ if iswindows:
|
||||
aes = AES(keykey)
|
||||
userkey = aes.decrypt(userkey)
|
||||
userkey = userkey[26:-ord(userkey[-1:])]
|
||||
#print "found key:",userkey.encode('hex')
|
||||
# print ("found " + uuid_name + " key: " + str(userkey))
|
||||
keys.append(userkey)
|
||||
names.append(uuid_name[9:])
|
||||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
print("Found {0:d} keys".format(len(keys)))
|
||||
return keys
|
||||
return keys, names
|
||||
|
||||
|
||||
elif isosx:
|
||||
@@ -429,19 +445,25 @@ elif isosx:
|
||||
tree = etree.parse(actpath)
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
|
||||
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
|
||||
userkey = tree.findtext(expr)
|
||||
userUUID = "Unknown"
|
||||
try:
|
||||
userUUID = tree.findtext(exprUUID)
|
||||
except:
|
||||
pass
|
||||
userkey = b64decode(userkey)
|
||||
userkey = userkey[26:]
|
||||
return [userkey]
|
||||
return [userkey], [userUUID[9:]]
|
||||
|
||||
else:
|
||||
def adeptkeys():
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
return [], []
|
||||
|
||||
# interface for Python DeDRM
|
||||
def getkey(outpath):
|
||||
keys = adeptkeys()
|
||||
keys, names = adeptkeys()
|
||||
if len(keys) > 0:
|
||||
if not os.path.isdir(outpath):
|
||||
outfile = outpath
|
||||
@@ -450,15 +472,17 @@ def getkey(outpath):
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
keycount = 0
|
||||
name_index = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount))
|
||||
outfile = os.path.join(outpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
name_index += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -504,7 +528,7 @@ def cli_main():
|
||||
# make sure the outpath is the
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
keys = adeptkeys()
|
||||
keys, names = adeptkeys()
|
||||
if len(keys) > 0:
|
||||
if not os.path.isdir(outpath):
|
||||
outfile = outpath
|
||||
@@ -513,15 +537,17 @@ def cli_main():
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
keycount = 0
|
||||
name_index = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount))
|
||||
outfile = os.path.join(outpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
name_index += 1
|
||||
else:
|
||||
print("Could not retrieve Adobe Adept key.")
|
||||
return 0
|
||||
@@ -554,12 +580,15 @@ def gui_main():
|
||||
progpath, progname = os.path.split(argv[0])
|
||||
success = False
|
||||
try:
|
||||
keys = adeptkeys()
|
||||
keys, names = adeptkeys()
|
||||
print(keys)
|
||||
print(names)
|
||||
keycount = 0
|
||||
name_index = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(progpath,"adobekey_{0:d}.der".format(keycount))
|
||||
outfile = os.path.join(progpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
|
||||
@@ -567,6 +596,7 @@ def gui_main():
|
||||
keyfileout.write(key)
|
||||
success = True
|
||||
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
|
||||
name_index += 1
|
||||
except ADEPTError as e:
|
||||
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
|
||||
except Exception:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# androidkindlekey.py
|
||||
# Copyright © 2010-20 by Thom, Apprentice et al.
|
||||
# Copyright © 2010-20 by Thom, Apprentice Harper et al.
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - AmazonSecureStorage.xml decryption to serial number
|
||||
@@ -30,10 +30,7 @@ import tempfile
|
||||
import zlib
|
||||
import tarfile
|
||||
from hashlib import md5
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from io import BytesIO as StringIO
|
||||
from io import BytesIO
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
# Routines common to Mac and PC
|
||||
@@ -48,11 +45,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -96,7 +99,7 @@ def unicode_argv():
|
||||
return ["kindlekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -116,7 +119,7 @@ class AndroidObfuscation(object):
|
||||
cipher = self._get_cipher()
|
||||
padding = len(self.key) - len(plaintext) % len(self.key)
|
||||
plaintext += chr(padding) * padding
|
||||
return b2a_hex(cipher.encrypt(plaintext))
|
||||
return b2a_hex(cipher.encrypt(plaintext.encode('utf-8')))
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
cipher = self._get_cipher()
|
||||
@@ -182,7 +185,7 @@ def get_serials1(path=STORAGE1):
|
||||
obfuscation = AndroidObfuscation()
|
||||
|
||||
def get_value(key):
|
||||
encrypted_key = obfuscation.encrypt(a2b_hex(key))
|
||||
encrypted_key = obfuscation.encrypt(key)
|
||||
encrypted_value = storage.get(encrypted_key)
|
||||
if encrypted_value:
|
||||
return obfuscation.decrypt(encrypted_value)
|
||||
@@ -198,6 +201,7 @@ def get_serials1(path=STORAGE1):
|
||||
try:
|
||||
tokens = set(get_value('kindle.account.tokens').split(','))
|
||||
except:
|
||||
sys.stderr.write('cannot get kindle account tokens\n')
|
||||
return []
|
||||
|
||||
serials = []
|
||||
@@ -251,8 +255,8 @@ def get_serials2(path=STORAGE2):
|
||||
|
||||
serials = []
|
||||
for x in dsns:
|
||||
serials.append(x)
|
||||
for y in tokens:
|
||||
serials.append(x)
|
||||
serials.append(y)
|
||||
serials.append(x+y)
|
||||
return serials
|
||||
@@ -277,7 +281,7 @@ def get_serials(path=STORAGE):
|
||||
read = open(path, 'rb')
|
||||
head = read.read(24)
|
||||
if head[:14] == b'ANDROID BACKUP':
|
||||
output = StringIO(zlib.decompress(read.read()))
|
||||
output = BytesIO(zlib.decompress(read.read()))
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
@@ -314,7 +318,7 @@ def getkey(outfile, inpath):
|
||||
if len(keys) > 0:
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
for key in keys:
|
||||
keyfileout.write(b2a_hex(key))
|
||||
keyfileout.write(key)
|
||||
keyfileout.write("\n")
|
||||
return True
|
||||
return False
|
||||
@@ -391,7 +395,7 @@ def gui_main():
|
||||
import tkinter.filedialog
|
||||
except:
|
||||
print("tkinter not installed")
|
||||
return cli_main()
|
||||
return 0
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
|
||||
@@ -41,7 +41,7 @@ def unicode_argv():
|
||||
return ["DeDRM.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
def add_cp65001_codec():
|
||||
|
||||
@@ -9,8 +9,9 @@ __license__ = 'GPL v3'
|
||||
import os, traceback, json, codecs
|
||||
|
||||
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
|
||||
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
|
||||
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
|
||||
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
|
||||
QCheckBox)
|
||||
|
||||
from PyQt5 import Qt as QtGui
|
||||
from zipfile import ZipFile
|
||||
@@ -29,6 +30,37 @@ from calibre_plugins.dedrm.utilities import uStrCmp
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
|
||||
|
||||
def checkForDeACSMkeys():
|
||||
try:
|
||||
from calibre_plugins.deacsm.libadobeAccount import exportAccountEncryptionKeyDER, getAccountUUID
|
||||
from calibre.ptempfile import TemporaryFile
|
||||
|
||||
|
||||
acc_uuid = getAccountUUID()
|
||||
if acc_uuid is None:
|
||||
return None, None
|
||||
|
||||
name = "DeACSM_uuid_" + getAccountUUID()
|
||||
|
||||
# Unfortunately, the DeACSM plugin only has code to export to a file, not to return raw key bytes.
|
||||
# Make a temporary file, have the plugin write to that, then read (& delete) that file.
|
||||
|
||||
with TemporaryFile(suffix='.der') as tmp_key_file:
|
||||
export_result = exportAccountEncryptionKeyDER(tmp_key_file)
|
||||
|
||||
if (export_result is False):
|
||||
return None, None
|
||||
|
||||
# Read key file
|
||||
with open(tmp_key_file,'rb') as keyfile:
|
||||
new_key_value = keyfile.read()
|
||||
|
||||
return new_key_value, name
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return None, None
|
||||
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
def __init__(self, plugin_path, alfdir):
|
||||
QWidget.__init__(self)
|
||||
@@ -50,6 +82,9 @@ class ConfigWidget(QWidget):
|
||||
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
|
||||
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
|
||||
self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix']
|
||||
self.tempdedrmprefs['deobfuscate_fonts'] = self.dedrmprefs['deobfuscate_fonts']
|
||||
self.tempdedrmprefs['remove_watermarks'] = self.dedrmprefs['remove_watermarks']
|
||||
self.tempdedrmprefs['lcp_passphrases'] = list(self.dedrmprefs['lcp_passphrases'])
|
||||
|
||||
# Start Qt Gui dialog layout
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -100,6 +135,10 @@ class ConfigWidget(QWidget):
|
||||
self.ereader_button.setToolTip(_("Click to manage keys for eReader ebooks"))
|
||||
self.ereader_button.setText("eReader ebooks")
|
||||
self.ereader_button.clicked.connect(self.ereader_keys)
|
||||
self.lcp_button = QtGui.QPushButton(self)
|
||||
self.lcp_button.setToolTip(_("Click to manage passphrases for Readium LCP ebooks"))
|
||||
self.lcp_button.setText("Readium LCP ebooks")
|
||||
self.lcp_button.clicked.connect(self.readium_lcp_keys)
|
||||
button_layout.addWidget(self.kindle_serial_button)
|
||||
button_layout.addWidget(self.kindle_android_button)
|
||||
button_layout.addWidget(self.bandn_button)
|
||||
@@ -107,6 +146,17 @@ class ConfigWidget(QWidget):
|
||||
button_layout.addWidget(self.ereader_button)
|
||||
button_layout.addWidget(self.adept_button)
|
||||
button_layout.addWidget(self.kindle_key_button)
|
||||
button_layout.addWidget(self.lcp_button)
|
||||
|
||||
self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts"))
|
||||
self.chkFontObfuscation.setToolTip("Deobfuscates fonts in EPUB files after DRM removal")
|
||||
self.chkFontObfuscation.setChecked(self.tempdedrmprefs["deobfuscate_fonts"])
|
||||
button_layout.addWidget(self.chkFontObfuscation)
|
||||
|
||||
self.chkRemoveWatermarks = QtGui.QCheckBox(_("Remove watermarks"))
|
||||
self.chkRemoveWatermarks.setToolTip("Tries to remove watermarks from files")
|
||||
self.chkRemoveWatermarks.setChecked(self.tempdedrmprefs["remove_watermarks"])
|
||||
button_layout.addWidget(self.chkRemoveWatermarks)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
@@ -148,6 +198,10 @@ class ConfigWidget(QWidget):
|
||||
d = ManageKeysDialog(self,"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63')
|
||||
d.exec_()
|
||||
|
||||
def readium_lcp_keys(self):
|
||||
d = ManageKeysDialog(self,"Readium LCP passphrase",self.tempdedrmprefs['lcp_passphrases'], AddLCPKeyDialog)
|
||||
d.exec_()
|
||||
|
||||
def help_link_activated(self, url):
|
||||
def get_help_file_resource():
|
||||
# Copy the HTML helpfile to the plugin directory each time the
|
||||
@@ -170,12 +224,15 @@ class ConfigWidget(QWidget):
|
||||
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
|
||||
self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix'])
|
||||
self.dedrmprefs.set('configured', True)
|
||||
self.dedrmprefs.set('deobfuscate_fonts', self.chkFontObfuscation.isChecked())
|
||||
self.dedrmprefs.set('remove_watermarks', self.chkRemoveWatermarks.isChecked())
|
||||
self.dedrmprefs.set('lcp_passphrases', self.tempdedrmprefs['lcp_passphrases'])
|
||||
self.dedrmprefs.writeprefs()
|
||||
|
||||
def load_resource(self, name):
|
||||
with ZipFile(self.plugin_path, 'r') as zf:
|
||||
if name in zf.namelist():
|
||||
return zf.read(name)
|
||||
return zf.read(name).decode('utf-8')
|
||||
return ""
|
||||
|
||||
|
||||
@@ -290,6 +347,8 @@ class ManageKeysDialog(QDialog):
|
||||
for key in self.plugin_keys:
|
||||
self.listy.addItem(QListWidgetItem(key))
|
||||
|
||||
self.listy.setMinimumWidth(self.listy.sizeHintForColumn(0) + 20)
|
||||
|
||||
def add_key(self):
|
||||
d = self.create_key(self)
|
||||
d.exec_()
|
||||
@@ -451,7 +510,6 @@ class ManageKeysDialog(QDialog):
|
||||
|
||||
class RenameKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
print(repr(self), repr(parent))
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name))
|
||||
@@ -502,12 +560,6 @@ class RenameKeyDialog(QDialog):
|
||||
return str(self.key_ledit.text()).strip()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class AddBandNKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
@@ -560,6 +612,11 @@ class AddBandNKeyDialog(QDialog):
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self.chkOldAlgo = QCheckBox(_("Try to use the old algorithm"))
|
||||
self.chkOldAlgo.setToolTip(_("Leave this off if you're unsure."))
|
||||
data_group_box_layout.addWidget(self.chkOldAlgo)
|
||||
layout.addSpacing(10)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Retrieved key:", self))
|
||||
@@ -597,13 +654,25 @@ class AddBandNKeyDialog(QDialog):
|
||||
return str(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
if self.chkOldAlgo.isChecked():
|
||||
# old method, try to generate
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
|
||||
generated_key = generate_bandn_key(self.user_name, self.cc_number)
|
||||
if generated_key == "":
|
||||
errmsg = "Could not generate key."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(generated_key.decode("latin-1"))
|
||||
else:
|
||||
# New method, try to connect to server
|
||||
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
|
||||
fetched_key = fetch_bandn_key(self.user_name,self. cc_number)
|
||||
if fetched_key == "":
|
||||
errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(fetched_key)
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
@@ -709,21 +778,52 @@ class AddAdeptDialog(QDialog):
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
|
||||
defaultkeys = adeptkeys()
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(parent.parent.alfdir,"adobekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".der",parent.getwineprefix())
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",parent.getwineprefix())
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
self.default_name_A = defaultnames[0]
|
||||
|
||||
for key in self.parent.plugin_keys.values():
|
||||
if key == codecs.encode(self.default_key,'hex').decode("utf-8"):
|
||||
# We already have the ADE key imported into the plugin.
|
||||
# Set it back to "" as if we had not found anything,
|
||||
# so the next code path searches more places for potential keys.
|
||||
print("Found key '{0}' in ADE - already present, skipping.".format(self.default_name_A))
|
||||
self.default_key = ""
|
||||
break
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
self.default_key = ""
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.foundInPlugin = False
|
||||
|
||||
|
||||
if len(self.default_key) == 0:
|
||||
# No (new) key found in ADE. Check the DeACSM calibre plugin instead.
|
||||
key, name = checkForDeACSMkeys()
|
||||
|
||||
if key is not None:
|
||||
self.default_key = key
|
||||
self.default_name_A = name
|
||||
|
||||
for key in self.parent.plugin_keys.values():
|
||||
if key == codecs.encode(self.default_key,'hex').decode("utf-8"):
|
||||
# We already have the ADE key imported into the plugin.
|
||||
# Set it back to "" as if we had not found anything,
|
||||
# so the next code path searches more places for potential keys.
|
||||
print("Found key '{0}' in DeACSM - already present, skipping.".format(self.default_name_A))
|
||||
self.default_key = ""
|
||||
break
|
||||
|
||||
|
||||
if len(self.default_key)>0:
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
|
||||
data_group_box = QGroupBox("", self)
|
||||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
@@ -732,13 +832,17 @@ class AddAdeptDialog(QDialog):
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit("default_key", self)
|
||||
self.key_ledit.setToolTip("<p>Enter an identifying name for the current default Adobe Digital Editions key.")
|
||||
self.key_ledit = QLineEdit(self.default_name_A, self)
|
||||
self.key_ledit.setToolTip("<p>Enter an identifying name for the current Adobe key. Note that it's recommended to leave the UUID (the random-looking digits and letters) as it is.")
|
||||
key_group.addWidget(self.key_ledit)
|
||||
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
else:
|
||||
default_key_error = QLabel("The default encryption key for Adobe Digital Editions could not be found.", self)
|
||||
else:
|
||||
# No new key found - neither in ADE nor in the DeACSM plugin
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
|
||||
default_key_error = QLabel("No new ADE key could be found. Either ADE is not installed, or the key is already present in the plugin.", self)
|
||||
default_key_error.setAlignment(Qt.AlignHCenter)
|
||||
layout.addWidget(default_key_error)
|
||||
# if no default, bot buttons do the same
|
||||
@@ -755,7 +859,7 @@ class AddAdeptDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
return codecs.encode(self.default_key,'hex')
|
||||
return codecs.encode(self.default_key,'hex').decode("utf-8")
|
||||
|
||||
|
||||
def accept(self):
|
||||
@@ -1013,3 +1117,43 @@ class AddPIDDialog(QDialog):
|
||||
QDialog.accept(self)
|
||||
|
||||
|
||||
class AddLCPKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Add new Readium LCP passphrase".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
data_group_box = QGroupBox("", self)
|
||||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Readium LCP passphrase:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip("Enter your Readium LCP passphrase")
|
||||
key_group.addWidget(self.key_ledit)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
return str(self.key_ledit.text())
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_value) == 0 or self.key_value.isspace():
|
||||
errmsg = "Please enter your LCP passphrase or click Cancel in the dialog."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
QDialog.accept(self)
|
||||
|
||||
@@ -15,11 +15,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
319
DeDRM_plugin/epubfontdecrypt.py
Normal file
319
DeDRM_plugin/epubfontdecrypt.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# epubfontdecrypt.py
|
||||
# Copyright © 2021 by noDRM
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
||||
"""
|
||||
Decrypts / deobfuscates font files in EPUB files
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "1"
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
from lxml import etree
|
||||
import itertools
|
||||
import hashlib
|
||||
import binascii
|
||||
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, obfuscationkeyIETF, obfuscationkeyAdobe, encryption):
|
||||
enc = lambda tag: '{%s}%s' % ('http://www.w3.org/2001/04/xmlenc#', tag)
|
||||
dsig = lambda tag: '{%s}%s' % ('http://www.w3.org/2000/09/xmldsig#', tag)
|
||||
self.obfuscation_key_Adobe = obfuscationkeyAdobe
|
||||
self.obfuscation_key_IETF = obfuscationkeyIETF
|
||||
|
||||
self._encryption = etree.fromstring(encryption)
|
||||
# This loops through all entries in the "encryption.xml" file
|
||||
# to figure out which files need to be decrypted.
|
||||
self._obfuscatedIETF = obfuscatedIETF = set()
|
||||
self._obfuscatedAdobe = obfuscatedAdobe = set()
|
||||
self._other = other = set()
|
||||
|
||||
self._json_elements_to_remove = json_elements_to_remove = set()
|
||||
self._has_remaining_xml = False
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in self._encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
|
||||
if path is not None:
|
||||
|
||||
if encryption_type_url == "http://www.idpf.org/2008/embedding":
|
||||
# Font files obfuscated with the IETF algorithm
|
||||
path = path.encode('utf-8')
|
||||
obfuscatedIETF.add(path)
|
||||
if (self.obfuscation_key_IETF is None):
|
||||
self._has_remaining_xml = True
|
||||
else:
|
||||
json_elements_to_remove.add(elem.getparent().getparent())
|
||||
|
||||
elif encryption_type_url == "http://ns.adobe.com/pdf/enc#RC":
|
||||
# Font files obfuscated with the Adobe algorithm.
|
||||
path = path.encode('utf-8')
|
||||
obfuscatedAdobe.add(path)
|
||||
if (self.obfuscation_key_Adobe is None):
|
||||
self._has_remaining_xml = True
|
||||
else:
|
||||
json_elements_to_remove.add(elem.getparent().getparent())
|
||||
|
||||
else:
|
||||
path = path.encode('utf-8')
|
||||
other.add(path)
|
||||
self._has_remaining_xml = True
|
||||
# Other unsupported type.
|
||||
|
||||
for elem in json_elements_to_remove:
|
||||
elem.getparent().remove(elem)
|
||||
|
||||
def check_if_remaining(self):
|
||||
return self._has_remaining_xml
|
||||
|
||||
def get_xml(self):
|
||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
try:
|
||||
decompressed_bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
decompressed_bytes = decompressed_bytes + ex
|
||||
except:
|
||||
# possibly not compressed by zip - just return bytes
|
||||
return bytes, False
|
||||
return decompressed_bytes , True
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path.encode('utf-8') in self._obfuscatedIETF and self.obfuscation_key_IETF is not None:
|
||||
# de-obfuscate according to the IETF standard
|
||||
data, was_decomp = self.decompress(data)
|
||||
|
||||
if len(data) <= 1040:
|
||||
# de-obfuscate whole file
|
||||
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data)
|
||||
else:
|
||||
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data[:1040]) + data[1040:]
|
||||
|
||||
if (not was_decomp):
|
||||
out, was_decomp = self.decompress(out)
|
||||
return out
|
||||
|
||||
elif path.encode('utf-8') in self._obfuscatedAdobe and self.obfuscation_key_Adobe is not None:
|
||||
# de-obfuscate according to the Adobe standard
|
||||
data, was_decomp = self.decompress(data)
|
||||
|
||||
if len(data) <= 1024:
|
||||
# de-obfuscate whole file
|
||||
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data)
|
||||
else:
|
||||
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data[:1024]) + data[1024:]
|
||||
|
||||
if (not was_decomp):
|
||||
out, was_decomp = self.decompress(out)
|
||||
return out
|
||||
|
||||
else:
|
||||
# Not encrypted or obfuscated
|
||||
return data
|
||||
|
||||
def deobfuscate_single_data(self, key, data):
|
||||
try:
|
||||
msg = bytes([c^k for c,k in zip(data, itertools.cycle(key))])
|
||||
except TypeError:
|
||||
# Python 2
|
||||
msg = ''.join(chr(ord(c)^ord(k)) for c,k in itertools.izip(data, itertools.cycle(key)))
|
||||
return msg
|
||||
|
||||
|
||||
|
||||
def decryptFontsBook(inpath, outpath):
|
||||
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = inf.namelist()
|
||||
if 'META-INF/encryption.xml' not in namelist:
|
||||
return 1
|
||||
|
||||
# Font key handling:
|
||||
|
||||
font_master_key = None
|
||||
adobe_master_encryption_key = None
|
||||
|
||||
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
|
||||
path = None
|
||||
|
||||
try:
|
||||
container = etree.fromstring(inf.read("META-INF/container.xml"))
|
||||
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
|
||||
for rootfile in rootfiles:
|
||||
path = rootfile.get("full-path", None)
|
||||
if (path is not None):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# If path is None, we didn't find an OPF, so we probably don't have a font key.
|
||||
# If path is set, it's the path to the main content OPF file.
|
||||
|
||||
if (path is None):
|
||||
print("FontDecrypt: No OPF for font obfuscation found")
|
||||
return 1
|
||||
else:
|
||||
packageNS = lambda tag: '{%s}%s' % ('http://www.idpf.org/2007/opf', tag)
|
||||
metadataDCNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||
|
||||
try:
|
||||
container = etree.fromstring(inf.read(path))
|
||||
except:
|
||||
container = []
|
||||
|
||||
## IETF font key algorithm:
|
||||
print("FontDecrypt: Checking {0} for IETF font obfuscation keys ... ".format(path), end='')
|
||||
secret_key_name = None
|
||||
try:
|
||||
secret_key_name = container.get("unique-identifier")
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
identify_element = container.find(packageNS("metadata")).find(metadataDCNS("identifier"))
|
||||
if (secret_key_name is None or secret_key_name == identify_element.get("id")):
|
||||
font_master_key = identify_element.text
|
||||
except:
|
||||
pass
|
||||
|
||||
if (font_master_key is not None):
|
||||
if (secret_key_name is None):
|
||||
print("found '%s'" % (font_master_key))
|
||||
else:
|
||||
print("found '%s' (%s)" % (font_master_key, secret_key_name))
|
||||
|
||||
# Trim / remove forbidden characters from the key, then hash it:
|
||||
font_master_key = font_master_key.replace(' ', '')
|
||||
font_master_key = font_master_key.replace('\t', '')
|
||||
font_master_key = font_master_key.replace('\r', '')
|
||||
font_master_key = font_master_key.replace('\n', '')
|
||||
font_master_key = font_master_key.encode('utf-8')
|
||||
font_master_key = hashlib.sha1(font_master_key).digest()
|
||||
else:
|
||||
print("not found")
|
||||
|
||||
## Adobe font key algorithm
|
||||
print("FontDecrypt: Checking {0} for Adobe font obfuscation keys ... ".format(path), end='')
|
||||
|
||||
try:
|
||||
metadata = container.find(packageNS("metadata"))
|
||||
identifiers = metadata.findall(metadataDCNS("identifier"))
|
||||
|
||||
uid = None
|
||||
uidMalformed = False
|
||||
|
||||
for identifier in identifiers:
|
||||
if identifier.get(packageNS("scheme")) == "UUID":
|
||||
if identifier.text[:9] == "urn:uuid:":
|
||||
uid = identifier.text[9:]
|
||||
else:
|
||||
uid = identifier.text
|
||||
break
|
||||
if identifier.text[:9] == "urn:uuid:":
|
||||
uid = identifier.text[9:]
|
||||
break
|
||||
|
||||
|
||||
if uid is not None:
|
||||
uid = uid.replace(chr(0x20),'').replace(chr(0x09),'')
|
||||
uid = uid.replace(chr(0x0D),'').replace(chr(0x0A),'').replace('-','')
|
||||
|
||||
if len(uid) < 16:
|
||||
uidMalformed = True
|
||||
if not all(c in "0123456789abcdefABCDEF" for c in uid):
|
||||
uidMalformed = True
|
||||
|
||||
|
||||
if not uidMalformed:
|
||||
print("found '{0}'".format(uid))
|
||||
uid = uid + uid
|
||||
adobe_master_encryption_key = binascii.unhexlify(uid[:32])
|
||||
|
||||
if adobe_master_encryption_key is None:
|
||||
print("not found")
|
||||
|
||||
except:
|
||||
print("exception")
|
||||
pass
|
||||
|
||||
# Begin decrypting.
|
||||
|
||||
try:
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(font_master_key, adobe_master_encryption_key, encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
|
||||
# Mimetype needs to be the first entry, so remove it from the list
|
||||
# whereever it is, then add it at the beginning.
|
||||
namelist.remove("mimetype")
|
||||
|
||||
for path in (["mimetype"] + namelist):
|
||||
data = inf.read(path)
|
||||
zi = ZipInfo(path)
|
||||
zi.compress_type=ZIP_DEFLATED
|
||||
|
||||
if path == "mimetype":
|
||||
# mimetype must not be compressed
|
||||
zi.compress_type = ZIP_STORED
|
||||
|
||||
elif path == "META-INF/encryption.xml":
|
||||
# Check if there's still other entries not related to fonts
|
||||
if (decryptor.check_if_remaining()):
|
||||
data = decryptor.get_xml()
|
||||
print("FontDecrypt: There's remaining entries in encryption.xml, adding file ...")
|
||||
else:
|
||||
# No remaining entries, no need for that file.
|
||||
continue
|
||||
|
||||
try:
|
||||
# get the file info, including time-stamp
|
||||
oldzi = inf.getinfo(path)
|
||||
# copy across useful fields
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
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
|
||||
zi.flag_bits |= 0x800
|
||||
except:
|
||||
pass
|
||||
|
||||
if path == "mimetype":
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
elif path == "META-INF/encryption.xml":
|
||||
outf.writestr(zi, data)
|
||||
else:
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print("FontDecrypt: Could not decrypt fonts in {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||
traceback.print_exc()
|
||||
return 2
|
||||
return 0
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
|
||||
# 1.01 - Added routine for use by Windows DeDRM
|
||||
# 2.00 - Python 3, September 2020
|
||||
# 2.01 - Add new Adobe DRM, add Readium LCP
|
||||
#
|
||||
# Written in 2011 by Paul Durrant
|
||||
# Released with unlicense. See http://unlicense.org/
|
||||
@@ -66,11 +67,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -114,7 +121,7 @@ def unicode_argv():
|
||||
return ["epubtest.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
@@ -169,17 +176,23 @@ def getfiledata(file, zi):
|
||||
|
||||
def encryption(infile):
|
||||
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
|
||||
encryption = "Unknown"
|
||||
encryption = "Error When Checking."
|
||||
try:
|
||||
with open(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Zip
|
||||
if bookdata[0:0+2] == "PK":
|
||||
if bookdata[0:0+2] == b"PK":
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
if (
|
||||
'META-INF/encryption.xml' in namelist and
|
||||
'META-INF/license.lcpl' in namelist and
|
||||
b"EncryptedContentKey" in inzip.read("META-INF/encryption.xml")):
|
||||
encryption = "Readium LCP"
|
||||
|
||||
elif 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
|
||||
@@ -187,7 +200,9 @@ def encryption(infile):
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
encryption = "Adobe"
|
||||
encryption = "Adobe (old)"
|
||||
if len(bookkey) == 192:
|
||||
encryption = "Adobe (new)"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
@@ -198,7 +213,10 @@ def encryption(infile):
|
||||
|
||||
def main():
|
||||
argv=unicode_argv()
|
||||
print(encryption(argv[1]))
|
||||
if len(argv) < 2:
|
||||
print("Give an ePub file as a parameter.")
|
||||
else:
|
||||
print(encryption(argv[1]))
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
244
DeDRM_plugin/epubwatermark.py
Normal file
244
DeDRM_plugin/epubwatermark.py
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# epubwatermark.py
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Initial version
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
"""
|
||||
Removes various watermarks from EPUB files
|
||||
"""
|
||||
|
||||
import traceback
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
# Runs a RegEx over all HTML/XHTML files to remove watermakrs.
|
||||
def removeHTMLwatermarks(object, path_to_ebook):
|
||||
try:
|
||||
inf = ZipFile(open(path_to_ebook, 'rb'))
|
||||
namelist = inf.namelist()
|
||||
|
||||
modded_names = []
|
||||
modded_contents = []
|
||||
|
||||
for file in namelist:
|
||||
if not (file.endswith('.html') or file.endswith('.xhtml') or file.endswith('.xml')):
|
||||
continue
|
||||
|
||||
try:
|
||||
file_str = inf.read(file).decode("utf-8")
|
||||
str_new = file_str
|
||||
|
||||
# Remove Adobe ADEPT watermarks
|
||||
# Match optional newline at the beginning, then a "meta" tag with name = "Adept.expected.resource" or "Adept.resource"
|
||||
# and either a "value" or a "content" element with an Adobe UUID
|
||||
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s*\/>', '', str_new)
|
||||
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s*\/>', '', str_new)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
if (file_str == str_new):
|
||||
continue
|
||||
|
||||
modded_names.append(file)
|
||||
modded_contents.append(str_new)
|
||||
|
||||
if len(modded_names) == 0:
|
||||
# No file modified, return original
|
||||
return path_to_ebook
|
||||
|
||||
if len(modded_names) != len(modded_contents):
|
||||
# Something went terribly wrong, return original
|
||||
print("Watermark: Error during ADEPT watermark removal")
|
||||
return path_to_ebook
|
||||
|
||||
# Re-package with modified files:
|
||||
namelist.remove("mimetype")
|
||||
|
||||
try:
|
||||
output = object.temporary_file(".epub").name
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||
for path in (["mimetype"] + namelist):
|
||||
|
||||
data = inf.read(path)
|
||||
|
||||
try:
|
||||
modded_index = None
|
||||
modded_index = modded_names.index(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
if modded_index is not None:
|
||||
# Found modified file - replace contents
|
||||
data = modded_contents[modded_index]
|
||||
|
||||
zi = ZipInfo(path)
|
||||
oldzi = inf.getinfo(path)
|
||||
try:
|
||||
zi.compress_type = oldzi.compress_type
|
||||
if path == "mimetype":
|
||||
zi.compress_type = ZIP_STORED
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
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
|
||||
zi.flag_bits |= 0x800
|
||||
except:
|
||||
pass
|
||||
|
||||
outf.writestr(zi, data)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(len(modded_names)))
|
||||
return output
|
||||
|
||||
|
||||
# Finds the main OPF file, then uses RegEx to remove watermarks
|
||||
def removeOPFwatermarks(object, path_to_ebook):
|
||||
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
|
||||
opf_path = None
|
||||
|
||||
try:
|
||||
inf = ZipFile(open(path_to_ebook, 'rb'))
|
||||
container = etree.fromstring(inf.read("META-INF/container.xml"))
|
||||
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
|
||||
for rootfile in rootfiles:
|
||||
opf_path = rootfile.get("full-path", None)
|
||||
if (opf_path is not None):
|
||||
break
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
# If path is None, we didn't find an OPF, so we probably don't have a font key.
|
||||
# If path is set, it's the path to the main content OPF file.
|
||||
|
||||
if (opf_path is None):
|
||||
# No OPF found - no watermark
|
||||
return path_to_ebook
|
||||
else:
|
||||
try:
|
||||
container_str = inf.read(opf_path).decode("utf-8")
|
||||
container_str_new = container_str
|
||||
|
||||
# Remove Amazon hex watermarks
|
||||
# Match optional newline at the beginning, then spaces, then a "meta" tag with name = "Watermark" or "Watermark_(hex)" and a "content" element.
|
||||
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"Watermark(_\(hex\))?\"\s+content=\"[0-9a-fA-F]+\"\s*\/>', '', container_str_new)
|
||||
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+content=\"[0-9a-fA-F]+\"\s+name=\"Watermark(_\(hex\))?\"\s*\/>', '', container_str_new)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
if (container_str == container_str_new):
|
||||
# container didn't change - no watermark
|
||||
return path_to_ebook
|
||||
|
||||
# Re-package without watermark
|
||||
namelist = inf.namelist()
|
||||
namelist.remove("mimetype")
|
||||
|
||||
try:
|
||||
output = object.temporary_file(".epub").name
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||
for path in (["mimetype"] + namelist):
|
||||
|
||||
data = inf.read(path)
|
||||
if path == opf_path:
|
||||
# Found OPF, replacing ...
|
||||
data = container_str_new
|
||||
|
||||
zi = ZipInfo(path)
|
||||
oldzi = inf.getinfo(path)
|
||||
try:
|
||||
zi.compress_type = oldzi.compress_type
|
||||
if path == "mimetype":
|
||||
zi.compress_type = ZIP_STORED
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
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
|
||||
zi.flag_bits |= 0x800
|
||||
except:
|
||||
pass
|
||||
|
||||
outf.writestr(zi, data)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
|
||||
print("Watermark: Successfully stripped Amazon watermark from OPF file.")
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def removeCDPwatermark(object, path_to_ebook):
|
||||
# "META-INF/cdp.info" is a watermark file used by some Tolino vendors.
|
||||
# We don't want that in our eBooks, so lets remove that file.
|
||||
try:
|
||||
infile = ZipFile(open(path_to_ebook, 'rb'))
|
||||
namelist = infile.namelist()
|
||||
if 'META-INF/cdp.info' not in namelist:
|
||||
return path_to_ebook
|
||||
|
||||
namelist.remove("mimetype")
|
||||
namelist.remove("META-INF/cdp.info")
|
||||
|
||||
output = object.temporary_file(".epub").name
|
||||
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||
for path in (["mimetype"] + namelist):
|
||||
|
||||
data = infile.read(path)
|
||||
|
||||
zi = ZipInfo(path)
|
||||
oldzi = infile.getinfo(path)
|
||||
try:
|
||||
zi.compress_type = oldzi.compress_type
|
||||
if path == "mimetype":
|
||||
zi.compress_type = ZIP_STORED
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
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
|
||||
zi.flag_bits |= 0x800
|
||||
except:
|
||||
pass
|
||||
|
||||
outf.writestr(zi, data)
|
||||
|
||||
print("Watermark: Successfully removed cdp.info watermark")
|
||||
return output
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return path_to_ebook
|
||||
@@ -85,10 +85,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
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)
|
||||
|
||||
@@ -129,7 +136,7 @@ def unicode_argv():
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
Des = None
|
||||
if iswindows:
|
||||
@@ -200,7 +207,7 @@ class Sectionizer(object):
|
||||
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||
# Dictionary or normal content (TODO: Not hard-coded)
|
||||
if self.header[0x3C:0x3C+8] != ident:
|
||||
if self.header[0x3C:0x3C+8] == "PDctPPrs":
|
||||
if self.header[0x3C:0x3C+8] == b"PDctPPrs":
|
||||
self.bkType = "Dict"
|
||||
else:
|
||||
raise ValueError('Invalid file format')
|
||||
@@ -240,7 +247,7 @@ def sanitizeFileName(name):
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
return bytes([fixByte(a) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
@@ -269,13 +276,13 @@ class EreaderProcessor(object):
|
||||
raise ValueError('incorrect eReader version (error 2)')
|
||||
input = des.decrypt(data[-cookie_size:])
|
||||
def unshuff(data, shuf):
|
||||
r = [''] * len(data)
|
||||
r = [0] * len(data)
|
||||
j = 0
|
||||
for i in range(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len("".join(r)) == len(data)
|
||||
return "".join(r)
|
||||
assert len(bytes(r)) == len(data)
|
||||
return bytes(r)
|
||||
r = unshuff(input[0:-8], cookie_shuf)
|
||||
|
||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||
@@ -354,7 +361,7 @@ class EreaderProcessor(object):
|
||||
|
||||
def getImage(self, i):
|
||||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip('\0')
|
||||
name = sect[4:4+32].strip(b'\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name.decode('windows-1252')), data
|
||||
|
||||
@@ -404,7 +411,7 @@ class EreaderProcessor(object):
|
||||
|
||||
def getText(self):
|
||||
des = Des(fixKey(self.content_key))
|
||||
r = ''
|
||||
r = b''
|
||||
for i in range(self.num_text_pages):
|
||||
logging.debug('get page %d', i)
|
||||
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
|
||||
@@ -456,8 +463,7 @@ def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in range(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
pml2 = pml2.replace(bytes([k]), b'\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
@@ -476,7 +482,7 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print("Decoding File")
|
||||
sect =Sectionizer(infile, 'PNRdPPrs')
|
||||
sect =Sectionizer(infile, b'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
|
||||
@@ -559,7 +559,7 @@ class DocParser(object):
|
||||
if (link > 0):
|
||||
linktype = self.link_type[link-1]
|
||||
title = self.link_title[link-1]
|
||||
title = title.rstrip(b'. ')
|
||||
title = title.rstrip(b'. ').decode('utf-8')
|
||||
alt_title = parares[lstart:]
|
||||
alt_title = alt_title.strip()
|
||||
# now strip off the actual printed page number
|
||||
@@ -770,10 +770,10 @@ class DocParser(object):
|
||||
first_para_continued = False
|
||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||
if not pclass:
|
||||
if orig_regtype.endswith(b'.right') : pclass = 'cl-right'
|
||||
elif orig_regtype.endswith(b'.center') : pclass = 'cl-center'
|
||||
elif orig_regtype.endswith(b'.left') : pclass = 'cl-left'
|
||||
elif orig_regtype.endswith(b'.justify') : pclass = 'cl-justify'
|
||||
if orig_regtype.endswith(b'.right') : pclass = b'cl-right'
|
||||
elif orig_regtype.endswith(b'.center') : pclass = b'cl-center'
|
||||
elif orig_regtype.endswith(b'.left') : pclass = b'cl-left'
|
||||
elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify'
|
||||
if pclass and (ptype == 'full') and (len(pclass) >= 6):
|
||||
tag = 'p'
|
||||
if pclass[3:6] == b'h1-' : tag = 'h4'
|
||||
|
||||
@@ -14,11 +14,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ __version__ = "5.0"
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
@@ -51,10 +52,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
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)
|
||||
|
||||
@@ -96,7 +104,7 @@ def unicode_argv():
|
||||
return ["ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
@@ -151,7 +159,7 @@ def _load_crypto_libcrypto():
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = ("\x00" * self._blocksize)
|
||||
iv = (b'\x00' * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES decryption failed')
|
||||
@@ -164,7 +172,7 @@ def _load_crypto_pycrypto():
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
@@ -207,15 +215,15 @@ class Decryptor(object):
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
if bytes(path,'utf-8') in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = data[:-data[-1]]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
@@ -241,7 +249,7 @@ def ignobleBook(inpath):
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
|
||||
key = keyb64.decode('base64')[:16]
|
||||
key = base64.b64decode(keyb64)[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
@@ -259,8 +267,8 @@ def decryptBook(keyb64, inpath, outpath):
|
||||
if len(bookkey) != 64:
|
||||
print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
bookkey = aes.decrypt(base64.b64decode(bookkey))
|
||||
bookkey = bookkey[:-bookkey[-1]]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
|
||||
@@ -37,11 +37,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -85,7 +91,7 @@ def unicode_argv():
|
||||
return ["ignoblekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -95,7 +101,10 @@ def getNookLogFiles():
|
||||
logFiles = []
|
||||
found = False
|
||||
if iswindows:
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
# some 64 bit machines do not have the proper registry key for some reason
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
|
||||
@@ -44,11 +44,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -92,7 +98,7 @@ def unicode_argv():
|
||||
return ["ignoblekeyfetch.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
|
||||
@@ -54,11 +54,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -102,7 +108,7 @@ def unicode_argv():
|
||||
return ["ignoblekeygen.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# Revision history:
|
||||
# 0.1 - Initial alpha testing release 2020 by Pu D. Pud
|
||||
# 0.2 - Python 3 for calibre 5.0 (in testing)
|
||||
# 0.3 - More Python3 fixes
|
||||
|
||||
|
||||
"""
|
||||
@@ -21,16 +22,18 @@ Decrypts Barnes & Noble encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "0.2"
|
||||
__version__ = "0.3"
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import zlib
|
||||
import struct
|
||||
import hashlib
|
||||
from decimal import *
|
||||
from itertools import chain, islice
|
||||
from io import BytesIO
|
||||
from decimal import Decimal
|
||||
import itertools
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
@@ -43,11 +46,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -86,7 +95,7 @@ def unicode_argv():
|
||||
return ["ignoblepdf.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
@@ -236,7 +245,6 @@ def _load_crypto():
|
||||
ARC4, AES = _load_crypto()
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
# Do we generate cross reference streams on output?
|
||||
@@ -273,7 +281,7 @@ def nunpack(s, default=0):
|
||||
elif l == 2:
|
||||
return struct.unpack('>H', s)[0]
|
||||
elif l == 3:
|
||||
return struct.unpack('>L', '\x00'+s)[0]
|
||||
return struct.unpack('>L', bytes([0]) + s)[0]
|
||||
elif l == 4:
|
||||
return struct.unpack('>L', s)[0]
|
||||
else:
|
||||
@@ -305,7 +313,7 @@ class PSLiteral(PSObject):
|
||||
Use PSLiteralTable.intern() instead.
|
||||
'''
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.name = name.decode('utf-8')
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
@@ -324,7 +332,7 @@ class PSKeyword(PSObject):
|
||||
Use PSKeywordTable.intern() instead.
|
||||
'''
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.name = name.decode('utf-8')
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
@@ -354,12 +362,12 @@ PSLiteralTable = PSSymbolTable(PSLiteral)
|
||||
PSKeywordTable = PSSymbolTable(PSKeyword)
|
||||
LIT = PSLiteralTable.intern
|
||||
KWD = PSKeywordTable.intern
|
||||
KEYWORD_BRACE_BEGIN = KWD('{')
|
||||
KEYWORD_BRACE_END = KWD('}')
|
||||
KEYWORD_ARRAY_BEGIN = KWD('[')
|
||||
KEYWORD_ARRAY_END = KWD(']')
|
||||
KEYWORD_DICT_BEGIN = KWD('<<')
|
||||
KEYWORD_DICT_END = KWD('>>')
|
||||
KEYWORD_BRACE_BEGIN = KWD(b'{')
|
||||
KEYWORD_BRACE_END = KWD(b'}')
|
||||
KEYWORD_ARRAY_BEGIN = KWD(b'[')
|
||||
KEYWORD_ARRAY_END = KWD(b']')
|
||||
KEYWORD_DICT_BEGIN = KWD(b'<<')
|
||||
KEYWORD_DICT_END = KWD(b'>>')
|
||||
|
||||
|
||||
def literal_name(x):
|
||||
@@ -381,18 +389,18 @@ def keyword_name(x):
|
||||
|
||||
## PSBaseParser
|
||||
##
|
||||
EOL = re.compile(r'[\r\n]')
|
||||
SPC = re.compile(r'\s')
|
||||
NONSPC = re.compile(r'\S')
|
||||
HEX = re.compile(r'[0-9a-fA-F]')
|
||||
END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]')
|
||||
END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]')
|
||||
HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.')
|
||||
END_NUMBER = re.compile(r'[^0-9]')
|
||||
END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]')
|
||||
END_STRING = re.compile(r'[()\134]')
|
||||
OCT_STRING = re.compile(r'[0-7]')
|
||||
ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 }
|
||||
EOL = re.compile(br'[\r\n]')
|
||||
SPC = re.compile(br'\s')
|
||||
NONSPC = re.compile(br'\S')
|
||||
HEX = re.compile(br'[0-9a-fA-F]')
|
||||
END_LITERAL = re.compile(br'[#/%\[\]()<>{}\s]')
|
||||
END_HEX_STRING = re.compile(br'[^\s0-9a-fA-F]')
|
||||
HEX_PAIR = re.compile(br'[0-9a-fA-F]{2}|.')
|
||||
END_NUMBER = re.compile(br'[^0-9]')
|
||||
END_KEYWORD = re.compile(br'[#/%\[\]()<>{}\s]')
|
||||
END_STRING = re.compile(br'[()\\]')
|
||||
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 }
|
||||
|
||||
class PSBaseParser(object):
|
||||
|
||||
@@ -435,7 +443,7 @@ class PSBaseParser(object):
|
||||
self.fp.seek(pos)
|
||||
# reset the status for nextline()
|
||||
self.bufpos = pos
|
||||
self.buf = ''
|
||||
self.buf = b''
|
||||
self.charpos = 0
|
||||
# reset the status for nexttoken()
|
||||
self.parse1 = self.parse_main
|
||||
@@ -457,32 +465,37 @@ class PSBaseParser(object):
|
||||
if not m:
|
||||
return (self.parse_main, len(s))
|
||||
j = m.start(0)
|
||||
c = s[j]
|
||||
if isinstance(s[j], str):
|
||||
# Python 2
|
||||
c = s[j]
|
||||
else:
|
||||
# Python 3
|
||||
c = bytes([s[j]])
|
||||
self.tokenstart = self.bufpos+j
|
||||
if c == '%':
|
||||
self.token = '%'
|
||||
if c == b'%':
|
||||
self.token = c
|
||||
return (self.parse_comment, j+1)
|
||||
if c == '/':
|
||||
self.token = ''
|
||||
if c == b'/':
|
||||
self.token = b''
|
||||
return (self.parse_literal, j+1)
|
||||
if c in '-+' or c.isdigit():
|
||||
if c in b'-+' or c.isdigit():
|
||||
self.token = c
|
||||
return (self.parse_number, j+1)
|
||||
if c == '.':
|
||||
if c == b'.':
|
||||
self.token = c
|
||||
return (self.parse_decimal, j+1)
|
||||
if c.isalpha():
|
||||
self.token = c
|
||||
return (self.parse_keyword, j+1)
|
||||
if c == '(':
|
||||
self.token = ''
|
||||
if c == b'(':
|
||||
self.token = b''
|
||||
self.paren = 1
|
||||
return (self.parse_string, j+1)
|
||||
if c == '<':
|
||||
self.token = ''
|
||||
if c == b'<':
|
||||
self.token = b''
|
||||
return (self.parse_wopen, j+1)
|
||||
if c == '>':
|
||||
self.token = ''
|
||||
if c == b'>':
|
||||
self.token = b''
|
||||
return (self.parse_wclose, j+1)
|
||||
self.add_token(KWD(c))
|
||||
return (self.parse_main, j+1)
|
||||
@@ -509,20 +522,26 @@ class PSBaseParser(object):
|
||||
return (self.parse_literal, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
c = s[j]
|
||||
if c == '#':
|
||||
self.hex = ''
|
||||
if isinstance(s[j], str):
|
||||
c = s[j]
|
||||
else:
|
||||
c = bytes([s[j]])
|
||||
if c == b'#':
|
||||
self.hex = b''
|
||||
return (self.parse_literal_hex, j+1)
|
||||
self.add_token(LIT(self.token))
|
||||
self.add_token(PSLiteralTable.intern(self.token))
|
||||
return (self.parse_main, j)
|
||||
|
||||
def parse_literal_hex(self, s, i):
|
||||
c = s[i]
|
||||
if isinstance(s[i], str):
|
||||
c = s[i]
|
||||
else:
|
||||
c = bytes([s[i]])
|
||||
if HEX.match(c) and len(self.hex) < 2:
|
||||
self.hex += c
|
||||
return (self.parse_literal_hex, i+1)
|
||||
if self.hex:
|
||||
self.token += chr(int(self.hex, 16))
|
||||
self.token += bytes([int(self.hex, 16)])
|
||||
return (self.parse_literal, i)
|
||||
|
||||
def parse_number(self, s, i):
|
||||
@@ -532,8 +551,11 @@ class PSBaseParser(object):
|
||||
return (self.parse_number, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
c = s[j]
|
||||
if c == '.':
|
||||
if isinstance(s[j], str):
|
||||
c = s[j]
|
||||
else:
|
||||
c = bytes([s[j]])
|
||||
if c == b'.':
|
||||
self.token += c
|
||||
return (self.parse_decimal, j+1)
|
||||
try:
|
||||
@@ -549,7 +571,7 @@ class PSBaseParser(object):
|
||||
return (self.parse_decimal, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
self.add_token(Decimal(self.token))
|
||||
self.add_token(Decimal(self.token.decode('utf-8')))
|
||||
return (self.parse_main, j)
|
||||
|
||||
def parse_keyword(self, s, i):
|
||||
@@ -575,15 +597,18 @@ class PSBaseParser(object):
|
||||
return (self.parse_string, len(s))
|
||||
j = m.start(0)
|
||||
self.token += s[i:j]
|
||||
c = s[j]
|
||||
if c == '\\':
|
||||
if isinstance(s[j], str):
|
||||
c = s[j]
|
||||
else:
|
||||
c = bytes([s[j]])
|
||||
if c == b'\\':
|
||||
self.oct = ''
|
||||
return (self.parse_string_1, j+1)
|
||||
if c == '(':
|
||||
if c == b'(':
|
||||
self.paren += 1
|
||||
self.token += c
|
||||
return (self.parse_string, j+1)
|
||||
if c == ')':
|
||||
if c == b')':
|
||||
self.paren -= 1
|
||||
if self.paren:
|
||||
self.token += c
|
||||
@@ -591,42 +616,51 @@ class PSBaseParser(object):
|
||||
self.add_token(self.token)
|
||||
return (self.parse_main, j+1)
|
||||
def parse_string_1(self, s, i):
|
||||
c = s[i]
|
||||
if isinstance(s[i], str):
|
||||
c = s[i]
|
||||
else:
|
||||
c = bytes([s[i]])
|
||||
if OCT_STRING.match(c) and len(self.oct) < 3:
|
||||
self.oct += c
|
||||
return (self.parse_string_1, i+1)
|
||||
if self.oct:
|
||||
self.token += chr(int(self.oct, 8))
|
||||
self.token += bytes([int(self.oct, 8)])
|
||||
return (self.parse_string, i)
|
||||
if c in ESC_STRING:
|
||||
self.token += chr(ESC_STRING[c])
|
||||
self.token += bytes([ESC_STRING[c]])
|
||||
return (self.parse_string, i+1)
|
||||
|
||||
def parse_wopen(self, s, i):
|
||||
c = s[i]
|
||||
if isinstance(s[i], str):
|
||||
c = s[i]
|
||||
else:
|
||||
c = bytes([s[i]])
|
||||
if c.isspace() or HEX.match(c):
|
||||
return (self.parse_hexstring, i)
|
||||
if c == '<':
|
||||
if c == b'<':
|
||||
self.add_token(KEYWORD_DICT_BEGIN)
|
||||
i += 1
|
||||
return (self.parse_main, i)
|
||||
|
||||
def parse_wclose(self, s, i):
|
||||
c = s[i]
|
||||
if c == '>':
|
||||
if isinstance(s[i], str):
|
||||
c = s[i]
|
||||
else:
|
||||
c = bytes([s[i]])
|
||||
if c == b'>':
|
||||
self.add_token(KEYWORD_DICT_END)
|
||||
i += 1
|
||||
return (self.parse_main, i)
|
||||
|
||||
def parse_hexstring(self, s, i):
|
||||
m = END_HEX_STRING.search(s, i)
|
||||
if not m:
|
||||
m1 = END_HEX_STRING.search(s, i)
|
||||
if not m1:
|
||||
self.token += s[i:]
|
||||
return (self.parse_hexstring, len(s))
|
||||
j = m.start(0)
|
||||
j = m1.start(0)
|
||||
self.token += s[i:j]
|
||||
token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)),
|
||||
SPC.sub('', self.token))
|
||||
token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]),
|
||||
SPC.sub(b'', self.token))
|
||||
self.add_token(token)
|
||||
return (self.parse_main, j)
|
||||
|
||||
@@ -641,15 +675,15 @@ class PSBaseParser(object):
|
||||
'''
|
||||
Fetches a next line that ends either with \\r or \\n.
|
||||
'''
|
||||
linebuf = ''
|
||||
linebuf = b''
|
||||
linepos = self.bufpos + self.charpos
|
||||
eol = False
|
||||
while 1:
|
||||
self.fillbuf()
|
||||
if eol:
|
||||
c = self.buf[self.charpos]
|
||||
c = bytes([self.buf[self.charpos]])
|
||||
# handle '\r\n'
|
||||
if c == '\n':
|
||||
if c == b'\n':
|
||||
linebuf += c
|
||||
self.charpos += 1
|
||||
break
|
||||
@@ -657,7 +691,7 @@ class PSBaseParser(object):
|
||||
if m:
|
||||
linebuf += self.buf[self.charpos:m.end(0)]
|
||||
self.charpos = m.end(0)
|
||||
if linebuf[-1] == '\r':
|
||||
if bytes([linebuf[-1]]) == b'\r':
|
||||
eol = True
|
||||
else:
|
||||
break
|
||||
@@ -673,7 +707,7 @@ class PSBaseParser(object):
|
||||
'''
|
||||
self.fp.seek(0, 2)
|
||||
pos = self.fp.tell()
|
||||
buf = ''
|
||||
buf = b''
|
||||
while 0 < pos:
|
||||
prevpos = pos
|
||||
pos = max(0, pos-self.BUFSIZ)
|
||||
@@ -681,13 +715,13 @@ class PSBaseParser(object):
|
||||
s = self.fp.read(prevpos-pos)
|
||||
if not s: break
|
||||
while 1:
|
||||
n = max(s.rfind('\r'), s.rfind('\n'))
|
||||
n = max(s.rfind(b'\r'), s.rfind(b'\n'))
|
||||
if n == -1:
|
||||
buf = s + buf
|
||||
break
|
||||
yield s[n:]+buf
|
||||
s = s[:n]
|
||||
buf = ''
|
||||
buf = b''
|
||||
return
|
||||
|
||||
|
||||
@@ -743,7 +777,7 @@ class PSStackParser(PSBaseParser):
|
||||
|
||||
def nextobject(self, direct=False):
|
||||
'''
|
||||
Yields a list of objects: keywords, literals, strings,
|
||||
Yields a list of objects: keywords, literals, strings (byte arrays),
|
||||
numbers, arrays and dictionaries. Arrays and dictionaries
|
||||
are represented as Python sequence and dictionaries.
|
||||
'''
|
||||
@@ -753,6 +787,8 @@ class PSStackParser(PSBaseParser):
|
||||
if (isinstance(token, int) or
|
||||
isinstance(token, Decimal) or
|
||||
isinstance(token, bool) or
|
||||
isinstance(token, bytearray) or
|
||||
isinstance(token, bytes) or
|
||||
isinstance(token, str) or
|
||||
isinstance(token, PSLiteral)):
|
||||
# normal token
|
||||
@@ -796,10 +832,10 @@ class PSStackParser(PSBaseParser):
|
||||
return obj
|
||||
|
||||
|
||||
LITERAL_CRYPT = PSLiteralTable.intern('Crypt')
|
||||
LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl'))
|
||||
LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW'))
|
||||
LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85'))
|
||||
LITERAL_CRYPT = PSLiteralTable.intern(b'Crypt')
|
||||
LITERALS_FLATE_DECODE = (PSLiteralTable.intern(b'FlateDecode'), PSLiteralTable.intern(b'Fl'))
|
||||
LITERALS_LZW_DECODE = (PSLiteralTable.intern(b'LZWDecode'), PSLiteralTable.intern(b'LZW'))
|
||||
LITERALS_ASCII85_DECODE = (PSLiteralTable.intern(b'ASCII85Decode'), PSLiteralTable.intern(b'A85'))
|
||||
|
||||
|
||||
## PDF Objects
|
||||
@@ -853,7 +889,7 @@ def resolve_all(x):
|
||||
if isinstance(x, list):
|
||||
x = [ resolve_all(v) for v in x ]
|
||||
elif isinstance(x, dict):
|
||||
for (k,v) in x.iteritems():
|
||||
for (k,v) in iter(x.items()):
|
||||
x[k] = resolve_all(v)
|
||||
return x
|
||||
|
||||
@@ -861,13 +897,13 @@ def decipher_all(decipher, objid, genno, x):
|
||||
'''
|
||||
Recursively decipher X.
|
||||
'''
|
||||
if isinstance(x, str):
|
||||
if isinstance(x, bytearray) or isinstance(x,bytes) or isinstance(x,str):
|
||||
return decipher(objid, genno, x)
|
||||
decf = lambda v: decipher_all(decipher, objid, genno, v)
|
||||
if isinstance(x, list):
|
||||
x = [decf(v) for v in x]
|
||||
elif isinstance(x, dict):
|
||||
x = dict((k, decf(v)) for (k, v) in x.iteritems())
|
||||
x = dict((k, decf(v)) for (k, v) in iter(x.items()))
|
||||
return x
|
||||
|
||||
|
||||
@@ -898,7 +934,7 @@ def num_value(x):
|
||||
|
||||
def str_value(x):
|
||||
x = resolve1(x)
|
||||
if not isinstance(x, str):
|
||||
if not (isinstance(x, bytearray) or isinstance(x, bytes) or isinstance(x, str)):
|
||||
if STRICT:
|
||||
raise PDFTypeError('String required: %r' % x)
|
||||
return ''
|
||||
@@ -931,18 +967,18 @@ def stream_value(x):
|
||||
# ascii85decode(data)
|
||||
def ascii85decode(data):
|
||||
n = b = 0
|
||||
out = ''
|
||||
out = b''
|
||||
for c in data:
|
||||
if '!' <= c and c <= 'u':
|
||||
if b'!' <= c and c <= b'u':
|
||||
n += 1
|
||||
b = b*85+(ord(c)-33)
|
||||
b = b*85+(c-33)
|
||||
if n == 5:
|
||||
out += struct.pack('>L',b)
|
||||
n = b = 0
|
||||
elif c == 'z':
|
||||
elif c == b'z':
|
||||
assert n == 0
|
||||
out += '\0\0\0\0'
|
||||
elif c == '~':
|
||||
out += b'\0\0\0\0'
|
||||
elif c == b'~':
|
||||
if n:
|
||||
for _ in range(5-n):
|
||||
b = b*85+84
|
||||
@@ -963,7 +999,7 @@ class PDFStream(PDFObject):
|
||||
cutdiv = len(rawdata) // 16
|
||||
rawdata = rawdata[:16*cutdiv]
|
||||
else:
|
||||
if eol in ('\r', '\n', '\r\n'):
|
||||
if eol in (b'\r', b'\n', b'\r\n'):
|
||||
rawdata = rawdata[:length]
|
||||
|
||||
self.dic = dic
|
||||
@@ -1009,7 +1045,7 @@ class PDFStream(PDFObject):
|
||||
# will get errors if the document is encrypted.
|
||||
data = zlib.decompress(data)
|
||||
elif f in LITERALS_LZW_DECODE:
|
||||
data = ''.join(LZWDecoder(BytesIO(data)).run())
|
||||
data = b''.join(LZWDecoder(BytesIO(data)).run())
|
||||
elif f in LITERALS_ASCII85_DECODE:
|
||||
data = ascii85decode(data)
|
||||
elif f == LITERAL_CRYPT:
|
||||
@@ -1031,14 +1067,14 @@ class PDFStream(PDFObject):
|
||||
raise PDFValueError(
|
||||
'Columns undefined for predictor=12')
|
||||
columns = int_value(params['Columns'])
|
||||
buf = ''
|
||||
ent0 = '\x00' * columns
|
||||
buf = b''
|
||||
ent0 = b'\x00' * columns
|
||||
for i in range(0, len(data), columns+1):
|
||||
pred = data[i]
|
||||
ent1 = data[i+1:i+1+columns]
|
||||
if pred == '\x02':
|
||||
ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \
|
||||
for (a,b) in zip(ent0,ent1))
|
||||
if pred == 2:
|
||||
ent1 = b''.join(bytes([(a+b) & 255]) \
|
||||
for (a,b) in zip(ent0,ent1))
|
||||
buf += ent1
|
||||
ent0 = ent1
|
||||
data = buf
|
||||
@@ -1072,11 +1108,11 @@ class PDFEncryptionError(PDFException): pass
|
||||
class PDFPasswordIncorrect(PDFEncryptionError): pass
|
||||
|
||||
# some predefined literals and keywords.
|
||||
LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm')
|
||||
LITERAL_XREF = PSLiteralTable.intern('XRef')
|
||||
LITERAL_PAGE = PSLiteralTable.intern('Page')
|
||||
LITERAL_PAGES = PSLiteralTable.intern('Pages')
|
||||
LITERAL_CATALOG = PSLiteralTable.intern('Catalog')
|
||||
LITERAL_OBJSTM = PSLiteralTable.intern(b'ObjStm')
|
||||
LITERAL_XREF = PSLiteralTable.intern(b'XRef')
|
||||
LITERAL_PAGE = PSLiteralTable.intern(b'Page')
|
||||
LITERAL_PAGES = PSLiteralTable.intern(b'Pages')
|
||||
LITERAL_CATALOG = PSLiteralTable.intern(b'Catalog')
|
||||
|
||||
|
||||
## XRefs
|
||||
@@ -1094,7 +1130,7 @@ class PDFXRef(object):
|
||||
return '<PDFXRef: objs=%d>' % len(self.offsets)
|
||||
|
||||
def objids(self):
|
||||
return self.offsets.iterkeys()
|
||||
return iter(self.offsets.keys())
|
||||
|
||||
def load(self, parser):
|
||||
self.offsets = {}
|
||||
@@ -1105,10 +1141,10 @@ class PDFXRef(object):
|
||||
raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
|
||||
if not line:
|
||||
raise PDFNoValidXRef('Premature eof: %r' % parser)
|
||||
if line.startswith('trailer'):
|
||||
if line.startswith(b'trailer'):
|
||||
parser.seek(pos)
|
||||
break
|
||||
f = line.strip().split(' ')
|
||||
f = line.strip().split(b' ')
|
||||
if len(f) != 2:
|
||||
raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line))
|
||||
try:
|
||||
@@ -1120,16 +1156,17 @@ class PDFXRef(object):
|
||||
(_, line) = parser.nextline()
|
||||
except PSEOF:
|
||||
raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
|
||||
f = line.strip().split(' ')
|
||||
f = line.strip().split(b' ')
|
||||
if len(f) != 3:
|
||||
raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line))
|
||||
(pos, genno, use) = f
|
||||
if use != 'n': continue
|
||||
self.offsets[objid] = (int(genno), int(pos))
|
||||
if use != b'n':
|
||||
continue
|
||||
self.offsets[objid] = (int(genno.decode('utf-8')), int(pos.decode('utf-8')))
|
||||
self.load_trailer(parser)
|
||||
return
|
||||
|
||||
KEYWORD_TRAILER = PSKeywordTable.intern('trailer')
|
||||
KEYWORD_TRAILER = PSKeywordTable.intern(b'trailer')
|
||||
def load_trailer(self, parser):
|
||||
try:
|
||||
(_,kwd) = parser.nexttoken()
|
||||
@@ -1180,8 +1217,8 @@ class PDFXRefStream(object):
|
||||
raise PDFNoValidXRef('Invalid PDF stream spec.')
|
||||
size = stream.dic['Size']
|
||||
index = stream.dic.get('Index', (0,size))
|
||||
self.index = zip(islice(index, 0, None, 2),
|
||||
islice(index, 1, None, 2))
|
||||
self.index = list(zip(itertools.islice(index, 0, None, 2),
|
||||
itertools.islice(index, 1, None, 2)))
|
||||
(self.fl1, self.fl2, self.fl3) = stream.dic['W']
|
||||
self.data = stream.get_data()
|
||||
self.entlen = self.fl1+self.fl2+self.fl3
|
||||
@@ -1234,7 +1271,8 @@ class PDFDocument(object):
|
||||
# set_parser(parser)
|
||||
# Associates the document with an (already initialized) parser object.
|
||||
def set_parser(self, parser):
|
||||
if self.parser: return
|
||||
if self.parser:
|
||||
return
|
||||
self.parser = parser
|
||||
# The document is set to be temporarily ready during collecting
|
||||
# all the basic information about the document, e.g.
|
||||
@@ -1256,13 +1294,13 @@ class PDFDocument(object):
|
||||
dict_value(trailer['Encrypt']))
|
||||
# fix for bad files
|
||||
except:
|
||||
self.encryption = ('ffffffffffffffffffffffffffffffffffff',
|
||||
self.encryption = (b'ffffffffffffffffffffffffffffffffffff',
|
||||
dict_value(trailer['Encrypt']))
|
||||
if 'Root' in trailer:
|
||||
self.set_root(dict_value(trailer['Root']))
|
||||
break
|
||||
else:
|
||||
raise PDFSyntaxError('No /Root object! - Is this really a PDF?')
|
||||
else:
|
||||
raise PDFSyntaxError('No /Root object! - Is this really a PDF?')
|
||||
# The document is set to be non-ready again, until all the
|
||||
# proper initialization (asking the password key and
|
||||
# verifying the access permission, so on) is finished.
|
||||
@@ -1283,7 +1321,7 @@ class PDFDocument(object):
|
||||
# Perform the initialization with a given password.
|
||||
# This step is mandatory even if there's no password associated
|
||||
# with the document.
|
||||
def initialize(self, password=''):
|
||||
def initialize(self, password=b''):
|
||||
if not self.encryption:
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
self.ready = True
|
||||
@@ -1310,14 +1348,14 @@ class PDFDocument(object):
|
||||
def genkey_adobe_ps(self, param):
|
||||
# nice little offline principal keys dictionary
|
||||
# global static principal key for German Onleihe / Bibliothek Digital
|
||||
principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')}
|
||||
principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')}
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
length = int_value(param.get('Length', 0)) / 8
|
||||
length = int_value(param.get('Length', 0)) // 8
|
||||
edcdata = str_value(param.get('EDCData')).decode('base64')
|
||||
pdrllic = str_value(param.get('PDRLLic')).decode('base64')
|
||||
pdrlpol = str_value(param.get('PDRLPol')).decode('base64')
|
||||
edclist = []
|
||||
for pair in edcdata.split('\n'):
|
||||
for pair in edcdata.split(b'\n'):
|
||||
edclist.append(pair)
|
||||
# principal key request
|
||||
for key in principalkeys:
|
||||
@@ -1326,20 +1364,20 @@ class PDFDocument(object):
|
||||
else:
|
||||
raise IGNOBLEError('Cannot find principal key for this pdf')
|
||||
shakey = SHA256(principalkey)
|
||||
ivector = 16 * chr(0)
|
||||
ivector = bytes(16)
|
||||
plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64'))
|
||||
if plaintext[-16:] != 16 * chr(16):
|
||||
if plaintext[-16:] != bytearray(b'\0x10')*16:
|
||||
raise IGNOBLEError('Offlinekey cannot be decrypted, aborting ...')
|
||||
pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol)
|
||||
if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16:
|
||||
if pdrlpol[-1] < 1 or pdrlpol[-1] > 16:
|
||||
raise IGNOBLEError('Could not decrypt PDRLPol, aborting ...')
|
||||
else:
|
||||
cutter = -1 * ord(pdrlpol[-1])
|
||||
cutter = -1 * pdrlpol[-1]
|
||||
pdrlpol = pdrlpol[:cutter]
|
||||
return plaintext[:16]
|
||||
|
||||
PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
|
||||
'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
|
||||
PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
|
||||
b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
|
||||
# experimental aes pw support
|
||||
def initialize_standard(self, password, docid, param):
|
||||
# copy from a global variable
|
||||
@@ -1356,7 +1394,7 @@ class PDFDocument(object):
|
||||
try:
|
||||
EncMetadata = str_value(param['EncryptMetadata'])
|
||||
except:
|
||||
EncMetadata = 'True'
|
||||
EncMetadata = b'True'
|
||||
self.is_printable = bool(P & 4)
|
||||
self.is_modifiable = bool(P & 8)
|
||||
self.is_extractable = bool(P & 16)
|
||||
@@ -1373,12 +1411,12 @@ class PDFDocument(object):
|
||||
hash.update(docid[0]) # 5
|
||||
# aes special handling if metadata isn't encrypted
|
||||
if EncMetadata == ('False' or 'false'):
|
||||
hash.update('ffffffff'.decode('hex'))
|
||||
hash.update(codecs.decode(b'ffffffff','hex'))
|
||||
if 5 <= R:
|
||||
# 8
|
||||
for _ in range(50):
|
||||
hash = hashlib.md5(hash.digest()[:length/8])
|
||||
key = hash.digest()[:length/8]
|
||||
hash = hashlib.md5(hash.digest()[:length//8])
|
||||
key = hash.digest()[:length//8]
|
||||
if R == 2:
|
||||
# Algorithm 3.4
|
||||
u1 = ARC4.new(key).decrypt(password)
|
||||
@@ -1388,7 +1426,7 @@ class PDFDocument(object):
|
||||
hash.update(docid[0]) # 3
|
||||
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
|
||||
for i in range(1,19+1):
|
||||
k = ''.join( chr(ord(c) ^ i) for c in key )
|
||||
k = b''.join(bytes([c ^ i]) for c in key )
|
||||
x = ARC4.new(k).decrypt(x)
|
||||
u1 = x+x # 32bytes total
|
||||
if R == 2:
|
||||
@@ -1410,9 +1448,9 @@ class PDFDocument(object):
|
||||
if V != 4:
|
||||
self.decipher = self.decipher_rc4 # XXX may be AES
|
||||
# aes
|
||||
elif V == 4 and Length == 128:
|
||||
elf.decipher = self.decipher_aes
|
||||
elif V == 4 and Length == 256:
|
||||
elif V == 4 and length == 128:
|
||||
self.decipher = self.decipher_aes
|
||||
elif V == 4 and length == 256:
|
||||
raise PDFNotImplementedError('AES256 encryption is currently unsupported')
|
||||
self.ready = True
|
||||
return
|
||||
@@ -1429,6 +1467,9 @@ class PDFDocument(object):
|
||||
bookkey = ''.join(rights.findtext(expr)).decode('base64')
|
||||
bookkey = aes.decrypt(bookkey)
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
# todo: Take a look at this.
|
||||
# This seems to be the only function that's different between ignoblepdf and ineptpdf.
|
||||
# A ton of useless duplicated code .....
|
||||
bookkey = bookkey[-16:]
|
||||
ebx_V = int_value(param.get('V', 4))
|
||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||
@@ -1440,18 +1481,18 @@ class PDFDocument(object):
|
||||
else:
|
||||
V = 2
|
||||
elif len(bookkey) == length + 1:
|
||||
V = ord(bookkey[0])
|
||||
V = bookkey[0]
|
||||
bookkey = bookkey[1:]
|
||||
else:
|
||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||
print("bookkey[0] is %d" % ord(bookkey[0]))
|
||||
print("bookkey[0] is %d" % bookkey[0])
|
||||
raise IGNOBLEError('error decrypting book session key - mismatched length')
|
||||
else:
|
||||
# proper length unknown try with whatever you have
|
||||
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
|
||||
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
|
||||
print("bookkey[0] is %d" % ord(bookkey[0]))
|
||||
print("bookkey[0] is %d" % bookkey[0])
|
||||
if ebx_V == 3:
|
||||
V = 3
|
||||
else:
|
||||
@@ -1475,7 +1516,7 @@ class PDFDocument(object):
|
||||
objid = struct.pack('<L', objid ^ 0x3569ac)
|
||||
genno = struct.pack('<L', genno ^ 0xca96)
|
||||
key = self.decrypt_key
|
||||
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + 'sAlT'
|
||||
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + b'sAlT'
|
||||
hash = hashlib.md5(key)
|
||||
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
|
||||
return key
|
||||
@@ -1484,7 +1525,7 @@ class PDFDocument(object):
|
||||
def genkey_v4(self, objid, genno):
|
||||
objid = struct.pack('<L', objid)[:3]
|
||||
genno = struct.pack('<L', genno)[:2]
|
||||
key = self.decrypt_key + objid + genno + 'sAlT'
|
||||
key = self.decrypt_key + objid + genno + b'sAlT'
|
||||
hash = hashlib.md5(key)
|
||||
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
|
||||
return key
|
||||
@@ -1495,8 +1536,7 @@ class PDFDocument(object):
|
||||
data = data[16:]
|
||||
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
|
||||
# remove pkcs#5 aes padding
|
||||
cutter = -1 * ord(plaintext[-1])
|
||||
#print(cutter)
|
||||
cutter = -1 * plaintext[-1]
|
||||
plaintext = plaintext[:cutter]
|
||||
return plaintext
|
||||
|
||||
@@ -1506,8 +1546,7 @@ class PDFDocument(object):
|
||||
data = data[16:]
|
||||
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
|
||||
# remove pkcs#5 aes padding
|
||||
cutter = -1 * ord(plaintext[-1])
|
||||
#print(cutter)
|
||||
cutter = -1 * plaintext[-1]
|
||||
plaintext = plaintext[:cutter]
|
||||
return plaintext
|
||||
|
||||
@@ -1516,7 +1555,7 @@ class PDFDocument(object):
|
||||
return ARC4.new(key).decrypt(data)
|
||||
|
||||
|
||||
KEYWORD_OBJ = PSKeywordTable.intern('obj')
|
||||
KEYWORD_OBJ = PSKeywordTable.intern(b'obj')
|
||||
|
||||
def getobj(self, objid):
|
||||
if not self.ready:
|
||||
@@ -1623,11 +1662,11 @@ class PDFParser(PSStackParser):
|
||||
def __repr__(self):
|
||||
return '<PDFParser>'
|
||||
|
||||
KEYWORD_R = PSKeywordTable.intern('R')
|
||||
KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj')
|
||||
KEYWORD_STREAM = PSKeywordTable.intern('stream')
|
||||
KEYWORD_XREF = PSKeywordTable.intern('xref')
|
||||
KEYWORD_STARTXREF = PSKeywordTable.intern('startxref')
|
||||
KEYWORD_R = PSKeywordTable.intern(b'R')
|
||||
KEYWORD_ENDOBJ = PSKeywordTable.intern(b'endobj')
|
||||
KEYWORD_STREAM = PSKeywordTable.intern(b'stream')
|
||||
KEYWORD_XREF = PSKeywordTable.intern(b'xref')
|
||||
KEYWORD_STARTXREF = PSKeywordTable.intern(b'startxref')
|
||||
def do_keyword(self, pos, token):
|
||||
if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF):
|
||||
self.add_results(*self.pop(1))
|
||||
@@ -1675,8 +1714,8 @@ class PDFParser(PSStackParser):
|
||||
if STRICT:
|
||||
raise PDFSyntaxError('Unexpected EOF')
|
||||
break
|
||||
if 'endstream' in line:
|
||||
i = line.index('endstream')
|
||||
if b'endstream' in line:
|
||||
i = line.index(b'endstream')
|
||||
objlen += i
|
||||
data += line[:i]
|
||||
break
|
||||
@@ -1696,7 +1735,7 @@ class PDFParser(PSStackParser):
|
||||
prev = None
|
||||
for line in self.revreadlines():
|
||||
line = line.strip()
|
||||
if line == 'startxref': break
|
||||
if line == b'startxref': break
|
||||
if line:
|
||||
prev = line
|
||||
else:
|
||||
@@ -1748,7 +1787,7 @@ class PDFParser(PSStackParser):
|
||||
except PDFNoValidXRef:
|
||||
# fallback
|
||||
self.seek(0)
|
||||
pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b')
|
||||
pat = re.compile(b'^(\\d+)\\s+(\\d+)\\s+obj\\b')
|
||||
offsets = {}
|
||||
xref = PDFXRef()
|
||||
while 1:
|
||||
@@ -1756,7 +1795,7 @@ class PDFParser(PSStackParser):
|
||||
(pos, line) = self.nextline()
|
||||
except PSEOF:
|
||||
break
|
||||
if line.startswith('trailer'):
|
||||
if line.startswith(b'trailer'):
|
||||
trailerpos = pos # remember last trailer
|
||||
m = pat.match(line)
|
||||
if not m: continue
|
||||
@@ -1783,7 +1822,7 @@ class PDFObjStrmParser(PDFParser):
|
||||
self.add_results(*self.popall())
|
||||
return
|
||||
|
||||
KEYWORD_R = KWD('R')
|
||||
KEYWORD_R = KWD(b'R')
|
||||
def do_keyword(self, pos, token):
|
||||
if token is self.KEYWORD_R:
|
||||
# reference to indirect object
|
||||
@@ -1826,7 +1865,7 @@ class PDFSerializer(object):
|
||||
def dump(self, outf):
|
||||
self.outf = outf
|
||||
self.write(self.version)
|
||||
self.write('\n%\xe2\xe3\xcf\xd3\n')
|
||||
self.write(b'\n%\xe2\xe3\xcf\xd3\n')
|
||||
doc = self.doc
|
||||
objids = self.objids
|
||||
xrefs = {}
|
||||
@@ -1848,18 +1887,18 @@ class PDFSerializer(object):
|
||||
startxref = self.tell()
|
||||
|
||||
if not gen_xref_stm:
|
||||
self.write('xref\n')
|
||||
self.write('0 %d\n' % (maxobj + 1,))
|
||||
self.write(b'xref\n')
|
||||
self.write(b'0 %d\n' % (maxobj + 1,))
|
||||
for objid in range(0, maxobj + 1):
|
||||
if objid in xrefs:
|
||||
# force the genno to be 0
|
||||
self.write("%010d 00000 n \n" % xrefs[objid][0])
|
||||
self.write(b"%010d 00000 n \n" % xrefs[objid][0])
|
||||
else:
|
||||
self.write("%010d %05d f \n" % (0, 65535))
|
||||
self.write(b"%010d %05d f \n" % (0, 65535))
|
||||
|
||||
self.write('trailer\n')
|
||||
self.write(b'trailer\n')
|
||||
self.serialize_object(trailer)
|
||||
self.write('\nstartxref\n%d\n%%%%EOF' % startxref)
|
||||
self.write(b'\nstartxref\n%d\n%%%%EOF' % startxref)
|
||||
|
||||
else: # Generate crossref stream.
|
||||
|
||||
@@ -1908,7 +1947,7 @@ class PDFSerializer(object):
|
||||
data.append(struct.pack('>L', f2)[-fl2:])
|
||||
data.append(struct.pack('>L', f3)[-fl3:])
|
||||
index.extend((first, prev - first + 1))
|
||||
data = zlib.compress(''.join(data))
|
||||
data = zlib.compress(b''.join(data))
|
||||
dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index,
|
||||
'W': [1, fl2, fl3], 'Length': len(data),
|
||||
'Filter': LITERALS_FLATE_DECODE[0],
|
||||
@@ -1917,7 +1956,7 @@ class PDFSerializer(object):
|
||||
dic['Info'] = trailer['Info']
|
||||
xrefstm = PDFStream(dic, data)
|
||||
self.serialize_indirect(maxobj, xrefstm)
|
||||
self.write('startxref\n%d\n%%%%EOF' % startxref)
|
||||
self.write(b'startxref\n%d\n%%%%EOF' % startxref)
|
||||
def write(self, data):
|
||||
self.outf.write(data)
|
||||
self.last = data[-1:]
|
||||
@@ -1926,13 +1965,10 @@ class PDFSerializer(object):
|
||||
return self.outf.tell()
|
||||
|
||||
def escape_string(self, string):
|
||||
string = string.replace('\\', '\\\\')
|
||||
string = string.replace('\n', r'\n')
|
||||
string = string.replace('(', r'\(')
|
||||
string = string.replace(')', r'\)')
|
||||
# get rid of ciando id
|
||||
regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}')
|
||||
if regularexp.match(string): return ('http://www.ciando.com')
|
||||
string = string.replace(b'\\', b'\\\\')
|
||||
string = string.replace(b'\n', b'\\n')
|
||||
string = string.replace(b'(', b'\\(')
|
||||
string = string.replace(b')', b'\\)')
|
||||
return string
|
||||
|
||||
def serialize_object(self, obj):
|
||||
@@ -1943,34 +1979,38 @@ class PDFSerializer(object):
|
||||
obj['Subtype'] = obj['Type']
|
||||
del obj['Type']
|
||||
# end - hope this doesn't have bad effects
|
||||
self.write('<<')
|
||||
self.write(b'<<')
|
||||
for key, val in obj.items():
|
||||
self.write('/%s' % key)
|
||||
self.write(str(PSLiteralTable.intern(key.encode('utf-8'))).encode('utf-8'))
|
||||
self.serialize_object(val)
|
||||
self.write('>>')
|
||||
self.write(b'>>')
|
||||
elif isinstance(obj, list):
|
||||
self.write('[')
|
||||
self.write(b'[')
|
||||
for val in obj:
|
||||
self.serialize_object(val)
|
||||
self.write(']')
|
||||
self.write(b']')
|
||||
elif isinstance(obj, bytearray):
|
||||
self.write(b'(%s)' % self.escape_string(obj))
|
||||
elif isinstance(obj, bytes):
|
||||
self.write(b'(%s)' % self.escape_string(obj))
|
||||
elif isinstance(obj, str):
|
||||
self.write('(%s)' % self.escape_string(obj))
|
||||
self.write(b'(%s)' % self.escape_string(obj.encode('utf-8')))
|
||||
elif isinstance(obj, bool):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj).lower())
|
||||
self.write(b' ')
|
||||
self.write(str(obj).lower().encode('utf-8'))
|
||||
elif isinstance(obj, (int, long)):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
self.write(b' ')
|
||||
self.write(str(obj).encode('utf-8'))
|
||||
elif isinstance(obj, Decimal):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write(str(obj))
|
||||
self.write(b' ')
|
||||
self.write(str(obj).encode('utf-8'))
|
||||
elif isinstance(obj, PDFObjRef):
|
||||
if self.last.isalnum():
|
||||
self.write(' ')
|
||||
self.write('%d %d R' % (obj.objid, 0))
|
||||
self.write(b' ')
|
||||
self.write(b'%d %d R' % (obj.objid, 0))
|
||||
elif isinstance(obj, PDFStream):
|
||||
### If we don't generate cross ref streams the object streams
|
||||
### are no longer useful, as we have extracted all objects from
|
||||
@@ -1980,21 +2020,21 @@ class PDFSerializer(object):
|
||||
else:
|
||||
data = obj.get_decdata()
|
||||
self.serialize_object(obj.dic)
|
||||
self.write('stream\n')
|
||||
self.write(b'stream\n')
|
||||
self.write(data)
|
||||
self.write('\nendstream')
|
||||
self.write(b'\nendstream')
|
||||
else:
|
||||
data = str(obj)
|
||||
if data[0].isalnum() and self.last.isalnum():
|
||||
self.write(' ')
|
||||
data = str(obj).encode('utf-8')
|
||||
if bytes([data[0]]).isalnum() and self.last.isalnum():
|
||||
self.write(b' ')
|
||||
self.write(data)
|
||||
|
||||
def serialize_indirect(self, objid, obj):
|
||||
self.write('%d 0 obj' % (objid,))
|
||||
self.write(b'%d 0 obj' % (objid,))
|
||||
self.serialize_object(obj)
|
||||
if self.last.isalnum():
|
||||
self.write('\n')
|
||||
self.write('endobj\n')
|
||||
self.write(b'\n')
|
||||
self.write(b'endobj\n')
|
||||
|
||||
|
||||
|
||||
@@ -2003,12 +2043,7 @@ def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
#try:
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
#except:
|
||||
# print("Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)))
|
||||
# return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
try:
|
||||
|
||||
160
DeDRM_plugin/ineptepub.py
Executable file → Normal file
160
DeDRM_plugin/ineptepub.py
Executable file → Normal file
@@ -46,7 +46,7 @@ import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
from lxml import etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -58,11 +58,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -104,12 +110,15 @@ def unicode_argv():
|
||||
return ["ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
class ADEPTNewVersionError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
@@ -311,7 +320,7 @@ def _load_crypto_pycrypto():
|
||||
total = 0
|
||||
for byte in bytes:
|
||||
total = (total << 8) + byte
|
||||
return long(total)
|
||||
return total
|
||||
|
||||
def decrypt(self, data):
|
||||
return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
|
||||
@@ -333,7 +342,7 @@ def _load_crypto():
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
@@ -343,22 +352,48 @@ class Decryptor(object):
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
self._otherData = otherData = set()
|
||||
|
||||
self._json_elements_to_remove = json_elements_to_remove = set()
|
||||
self._has_remaining_xml = False
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"):
|
||||
# Adobe
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
json_elements_to_remove.add(elem.getparent().getparent())
|
||||
else:
|
||||
path = path.encode('utf-8')
|
||||
otherData.add(path)
|
||||
self._has_remaining_xml = True
|
||||
|
||||
for elem in json_elements_to_remove:
|
||||
elem.getparent().remove(elem)
|
||||
|
||||
def check_if_remaining(self):
|
||||
return self._has_remaining_xml
|
||||
|
||||
def get_xml(self):
|
||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
try:
|
||||
decompressed_bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
decompressed_bytes = decompressed_bytes + ex
|
||||
except:
|
||||
# possibly not compressed by zip - just return bytes
|
||||
return bytes
|
||||
return decompressed_bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path.encode('utf-8') in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
@@ -389,12 +424,48 @@ def adeptBook(inpath):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Checks the license file and returns the UUID the book is licensed for.
|
||||
# This is used so that the Calibre plugin can pick the correct decryption key
|
||||
# first try without having to loop through all possible keys.
|
||||
def adeptGetUserUUID(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('user'),)
|
||||
user_uuid = ''.join(rights.findtext(expr))
|
||||
if user_uuid[:9] != "urn:uuid:":
|
||||
return None
|
||||
return user_uuid[9:]
|
||||
except:
|
||||
return None
|
||||
|
||||
def verify_book_key(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 decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
namelist = inf.namelist()
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
|
||||
@@ -406,41 +477,46 @@ def decryptBook(userkey, inpath, outpath):
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 192:
|
||||
print("{0:s} seems to be an Adobe ADEPT ePub with Adobe's new DRM".format(os.path.basename(inpath)))
|
||||
print("This DRM cannot be removed yet. ")
|
||||
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
|
||||
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
|
||||
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
|
||||
if len(bookkey) != 172:
|
||||
print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if len(bookkey) != 16:
|
||||
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
|
||||
if len(bookkey) > 16:
|
||||
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:
|
||||
bookkey = bookkey[-16:]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey, encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype')
|
||||
zi.compress_type=ZIP_STORED
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldzi = inf.getinfo('mimetype')
|
||||
# copy across fields to be preserved
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
|
||||
for path in (["mimetype"] + namelist):
|
||||
data = inf.read(path)
|
||||
zi = ZipInfo(path)
|
||||
zi.compress_type=ZIP_DEFLATED
|
||||
|
||||
if path == "mimetype":
|
||||
zi.compress_type = ZIP_STORED
|
||||
|
||||
elif path == "META-INF/encryption.xml":
|
||||
# Check if there's still something in there
|
||||
if (decryptor.check_if_remaining()):
|
||||
data = decryptor.get_xml()
|
||||
print("Adding encryption.xml for the remaining embedded files.")
|
||||
# We removed DRM, but there's still stuff like obfuscated fonts.
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
# get the file info, including time-stamp
|
||||
oldzi = inf.getinfo(path)
|
||||
@@ -452,9 +528,15 @@ def decryptBook(userkey, inpath, outpath):
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
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
|
||||
zi.flag_bits |= 0x800
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
if path == "META-INF/encryption.xml":
|
||||
outf.writestr(zi, data)
|
||||
else:
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||
return 2
|
||||
|
||||
537
DeDRM_plugin/ineptpdf.py
Normal file → Executable file
537
DeDRM_plugin/ineptpdf.py
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ import struct
|
||||
from io import BytesIO
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.py3compat import bchr, bord
|
||||
from Crypto.Util.py3compat import bchr
|
||||
|
||||
try:
|
||||
# lzma library from calibre 4.6.0 or later
|
||||
@@ -89,7 +89,7 @@ LEN_IS_VAR_LEN = 0xE
|
||||
LEN_IS_NULL = 0xF
|
||||
|
||||
|
||||
VERSION_MARKER = b"\x01\x00\xEA"
|
||||
VERSION_MARKER = [b"\x01", b"\x00", b"\xEA"]
|
||||
|
||||
|
||||
# asserts must always raise exceptions for proper functioning
|
||||
@@ -347,7 +347,7 @@ class BinaryIonParser(object):
|
||||
b = self.stream.read(1)
|
||||
if len(b) < 1:
|
||||
return -1
|
||||
b = bord(b)
|
||||
b = ord(b)
|
||||
result = b >> 4
|
||||
ln = b & 0xF
|
||||
|
||||
@@ -372,13 +372,13 @@ class BinaryIonParser(object):
|
||||
return result
|
||||
|
||||
def readvarint(self):
|
||||
b = bord(self.read())
|
||||
b = ord(self.read())
|
||||
negative = ((b & 0x40) != 0)
|
||||
result = (b & 0x3F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
b = ord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
@@ -389,12 +389,12 @@ class BinaryIonParser(object):
|
||||
return result
|
||||
|
||||
def readvaruint(self):
|
||||
b = bord(self.read())
|
||||
b = ord(self.read())
|
||||
result = (b & 0x7F)
|
||||
|
||||
i = 0
|
||||
while (b & 0x80) == 0 and i < 4:
|
||||
b = bord(self.read())
|
||||
b = ord(self.read())
|
||||
result = (result << 7) | (b & 0x7F)
|
||||
i += 1
|
||||
|
||||
@@ -414,7 +414,7 @@ class BinaryIonParser(object):
|
||||
_assert(self.localremaining <= 8, "Decimal overflow")
|
||||
|
||||
signed = False
|
||||
b = [bord(x) for x in self.read(self.localremaining)]
|
||||
b = [ord(x) for x in self.read(self.localremaining)]
|
||||
if (b[0] & 0x80) != 0:
|
||||
b[0] = b[0] & 0x7F
|
||||
signed = True
|
||||
@@ -512,6 +512,8 @@ class BinaryIonParser(object):
|
||||
|
||||
if table is not None:
|
||||
self.symbols.import_(table, min(maxid, len(table.symnames)))
|
||||
if len(table.symnames) < maxid:
|
||||
self.symbols.importunknown(name + "-unknown", maxid - len(table.symnames))
|
||||
else:
|
||||
self.symbols.importunknown(name, maxid)
|
||||
|
||||
@@ -579,7 +581,7 @@ class BinaryIonParser(object):
|
||||
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
|
||||
v = 0
|
||||
for i in range(self.valuelen - 1, -1, -1):
|
||||
v = (v | (bord(self.read()) << (i * 8)))
|
||||
v = (v | (ord(self.read()) << (i * 8)))
|
||||
|
||||
if self.valuetid == TID_NEGINT:
|
||||
self.value = -v
|
||||
@@ -649,7 +651,7 @@ class BinaryIonParser(object):
|
||||
|
||||
result = ""
|
||||
for i in b:
|
||||
result += ("%02x " % bord(i))
|
||||
result += ("%02x " % ord(i))
|
||||
|
||||
if len(result) > 0:
|
||||
result = result[:-1]
|
||||
@@ -733,7 +735,10 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
||||
'com.amazon.drm.EncryptedPage@2.0',
|
||||
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
|
||||
'com.amazon.drm.Compressed@1.0', 'page_index_table',
|
||||
'com.amazon.drm.VoucherEnvelope@2.0', 'com.amazon.drm.VoucherEnvelope@3.0' ]
|
||||
] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
|
||||
for n in list(range(2, 29)) + [
|
||||
9708, 1031, 2069, 9041, 3646,
|
||||
6052, 9479, 9888, 4648, 5683]]
|
||||
|
||||
def addprottable(ion):
|
||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||
@@ -748,7 +753,8 @@ def pkcs7pad(msg, blocklen):
|
||||
def pkcs7unpad(msg, blocklen):
|
||||
_assert(len(msg) % blocklen == 0)
|
||||
|
||||
paddinglen = bord(msg[-1])
|
||||
paddinglen = msg[-1]
|
||||
|
||||
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
|
||||
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
|
||||
|
||||
@@ -756,9 +762,45 @@ def pkcs7unpad(msg, blocklen):
|
||||
|
||||
|
||||
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
|
||||
VOUCHER_VERSION_INFOS = {
|
||||
2: [b'Antidisestablishmentarianism', 5],
|
||||
3: [b'Floccinaucinihilipilification', 8]
|
||||
OBFUSCATION_TABLE = {
|
||||
"V1": (0x00, None),
|
||||
"V2": (0x05, b'Antidisestablishmentarianism'),
|
||||
"V3": (0x08, b'Floccinaucinihilipilification'),
|
||||
"V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
||||
"V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
|
||||
"V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
|
||||
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
|
||||
"V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
|
||||
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
|
||||
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
|
||||
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
|
||||
"V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
|
||||
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
|
||||
"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]'),
|
||||
"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'),
|
||||
"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'),
|
||||
"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'),
|
||||
"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'),
|
||||
"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?'),
|
||||
"V1031": (0x08, b'Antidisestablishmentarianism'),
|
||||
"V2069": (0x07, b'Floccinaucinihilipilification'),
|
||||
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
||||
"V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
|
||||
"V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
|
||||
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||
}
|
||||
|
||||
|
||||
@@ -767,9 +809,7 @@ def obfuscate(secret, version):
|
||||
if version == 1: # v1 does not use obfuscation
|
||||
return secret
|
||||
|
||||
params = VOUCHER_VERSION_INFOS[version]
|
||||
word = params[0]
|
||||
magic = params[1]
|
||||
magic, word = OBFUSCATION_TABLE["V%d" % version]
|
||||
|
||||
# extend secret so that its length is divisible by the magic number
|
||||
if len(secret) % magic != 0:
|
||||
@@ -806,7 +846,13 @@ class DrmIonVoucher(object):
|
||||
secretkey = b""
|
||||
|
||||
def __init__(self, voucherenv, dsn, secret):
|
||||
self.dsn,self.secret = dsn,secret
|
||||
self.dsn, self.secret = dsn, secret
|
||||
|
||||
if isinstance(dsn, str):
|
||||
self.dsn = dsn.encode('ASCII')
|
||||
|
||||
if isinstance(secret, str):
|
||||
self.secret = secret.encode('ASCII')
|
||||
|
||||
self.lockparams = []
|
||||
|
||||
@@ -814,20 +860,20 @@ class DrmIonVoucher(object):
|
||||
addprottable(self.envelope)
|
||||
|
||||
def decryptvoucher(self):
|
||||
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
|
||||
shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII')
|
||||
|
||||
self.lockparams.sort()
|
||||
for param in self.lockparams:
|
||||
if param == "ACCOUNT_SECRET":
|
||||
shared += param + self.secret
|
||||
shared += param.encode('ASCII') + self.secret
|
||||
elif param == "CLIENT_ID":
|
||||
shared += param + self.dsn
|
||||
shared += param.encode('ASCII') + self.dsn
|
||||
else:
|
||||
_assert(False, "Unknown lock parameter: %s" % param)
|
||||
|
||||
sharedsecret = obfuscate(shared.encode('ASCII'), self.version)
|
||||
sharedsecret = obfuscate(shared, self.version)
|
||||
|
||||
key = hmac.new(sharedsecret, "PIDv3", digestmod=hashlib.sha256).digest()
|
||||
key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
|
||||
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
|
||||
b = aes.decrypt(self.ciphertext)
|
||||
b = pkcs7unpad(b, 16)
|
||||
@@ -992,6 +1038,7 @@ class DrmIon(object):
|
||||
|
||||
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
|
||||
decompress = False
|
||||
decrypt = True
|
||||
ct = None
|
||||
civ = None
|
||||
self.ion.stepin()
|
||||
@@ -1005,7 +1052,23 @@ class DrmIon(object):
|
||||
civ = self.ion.lobvalue()
|
||||
|
||||
if ct is not None and civ is not None:
|
||||
self.processpage(ct, civ, outpages, decompress)
|
||||
self.processpage(ct, civ, outpages, decompress, decrypt)
|
||||
self.ion.stepout()
|
||||
|
||||
elif self.ion.gettypename() in ["com.amazon.drm.PlainText@1.0", "com.amazon.drm.PlainText@2.0"]:
|
||||
decompress = False
|
||||
decrypt = False
|
||||
plaintext = None
|
||||
self.ion.stepin()
|
||||
while self.ion.hasnext():
|
||||
self.ion.next()
|
||||
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
|
||||
decompress = True
|
||||
if self.ion.getfieldname() == "data":
|
||||
plaintext = self.ion.lobvalue()
|
||||
|
||||
if plaintext is not None:
|
||||
self.processpage(plaintext, None, outpages, decompress, decrypt)
|
||||
self.ion.stepout()
|
||||
|
||||
self.ion.stepout()
|
||||
@@ -1016,15 +1079,18 @@ class DrmIon(object):
|
||||
def print_(self, lst):
|
||||
self.ion.print_(lst)
|
||||
|
||||
def processpage(self, ct, civ, outpages, decompress):
|
||||
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
|
||||
msg = pkcs7unpad(aes.decrypt(ct), 16)
|
||||
def processpage(self, ct, civ, outpages, decompress, decrypt):
|
||||
if decrypt:
|
||||
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
|
||||
msg = pkcs7unpad(aes.decrypt(ct), 16)
|
||||
else:
|
||||
msg = ct
|
||||
|
||||
if not decompress:
|
||||
outpages.write(msg)
|
||||
return
|
||||
|
||||
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported")
|
||||
_assert(msg[0] == 0, "LZMA UseFilter not supported")
|
||||
|
||||
if calibre_lzma is not None:
|
||||
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f:
|
||||
|
||||
@@ -103,11 +103,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -148,7 +154,7 @@ def unicode_argv():
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
@@ -191,7 +197,7 @@ def unescape(text):
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
text = chr(html.entities.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
# Engine to remove drm from Kindle KFX ebooks
|
||||
|
||||
# 2.0 - Python 3 for calibre 5.0
|
||||
# 2.1 - Some fixes for debugging
|
||||
# 2.1.1 - Whitespace!
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
import zipfile
|
||||
|
||||
from io import BytesIO
|
||||
try:
|
||||
from ion import DrmIon, DrmIonVoucher
|
||||
except:
|
||||
from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
@@ -27,22 +34,18 @@ class KFXZipBook:
|
||||
return (None, None)
|
||||
|
||||
def processBook(self, totalpids):
|
||||
try:
|
||||
import ion
|
||||
except:
|
||||
from calibre_plugins.dedrm import ion
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for filename in zf.namelist():
|
||||
with zf.open(filename) as fh:
|
||||
data = fh.read(8)
|
||||
if data != '\xeaDRMION\xee':
|
||||
if data != b'\xeaDRMION\xee':
|
||||
continue
|
||||
data += fh.read()
|
||||
if self.voucher is None:
|
||||
self.decrypt_voucher(totalpids)
|
||||
print("Decrypting KFX DRMION: {0}".format(filename))
|
||||
outfile = BytesIO()
|
||||
ion.DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
self.decrypted[filename] = outfile.getvalue()
|
||||
|
||||
if not self.decrypted:
|
||||
@@ -53,11 +56,11 @@ class KFXZipBook:
|
||||
for info in zf.infolist():
|
||||
with zf.open(info.filename) as fh:
|
||||
data = fh.read(4)
|
||||
if data != '\xe0\x01\x00\xea':
|
||||
if data != b'\xe0\x01\x00\xea':
|
||||
continue
|
||||
|
||||
data += fh.read()
|
||||
if 'ProtectedData' in data:
|
||||
if b'ProtectedData' in data:
|
||||
break # found DRM voucher
|
||||
else:
|
||||
raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher")
|
||||
@@ -65,6 +68,9 @@ class KFXZipBook:
|
||||
print("Decrypting KFX DRM voucher: {0}".format(info.filename))
|
||||
|
||||
for pid in [''] + totalpids:
|
||||
# Belt and braces. PIDs should be unicode strings, but just in case...
|
||||
if isinstance(pid, bytes):
|
||||
pid = pid.decode('ascii')
|
||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]:
|
||||
if len(pid) == dsn_len + secret_len:
|
||||
break # split pid into DSN and account secret
|
||||
@@ -72,11 +78,12 @@ class KFXZipBook:
|
||||
continue
|
||||
|
||||
try:
|
||||
voucher = ion.DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher.parse()
|
||||
voucher.decryptvoucher()
|
||||
break
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
else:
|
||||
raise Exception("Failed to decrypt KFX DRM voucher with any key")
|
||||
@@ -85,8 +92,10 @@ class KFXZipBook:
|
||||
|
||||
license_type = voucher.getlicensetype()
|
||||
if license_type != "Purchase":
|
||||
raise Exception(("This book is licensed as {0}. "
|
||||
'These tools are intended for use on purchased books.').format(license_type))
|
||||
#raise Exception(("This book is licensed as {0}. "
|
||||
# 'These tools are intended for use on purchased books.').format(license_type))
|
||||
print("Warning: This book is licensed as {0}. "
|
||||
"These tools are intended for use on purchased books. Continuing ...".format(license_type))
|
||||
|
||||
self.voucher = voucher
|
||||
|
||||
|
||||
@@ -174,14 +174,14 @@ def pidFromSerial(s, l):
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
if isinstance(serialnum,str):
|
||||
serialnum = serialnum.encode('utf-8')
|
||||
|
||||
if rec209 is None:
|
||||
return [serialnum]
|
||||
|
||||
pids=[]
|
||||
|
||||
if isinstance(serialnum,str):
|
||||
serialnum = serialnum.encode('utf-8')
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
@@ -209,7 +209,7 @@ def getK4Pids(rec209, token, kindleDatabase):
|
||||
kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens'])
|
||||
|
||||
except KeyError:
|
||||
kindleAccountToken=""
|
||||
kindleAccountToken = b''
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -296,14 +296,14 @@ def getPidList(md1, md2, serials=[], kDatabases=[]):
|
||||
|
||||
for kDatabase in kDatabases:
|
||||
try:
|
||||
pidlst.extend(getK4Pids(md1, md2, kDatabase))
|
||||
pidlst.extend(map(bytes,getK4Pids(md1, md2, kDatabase)))
|
||||
except Exception as e:
|
||||
print("Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]))
|
||||
traceback.print_exc()
|
||||
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||
pidlst.extend(map(bytes,getKindlePids(md1, md2, serialnum)))
|
||||
except Exception as e:
|
||||
print("Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]))
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -28,6 +28,7 @@ __version__ = '3.0'
|
||||
# 2.5 - Final Fix for Windows user names with non-ascii characters, thanks to oneofusoneofus
|
||||
# 2.6 - Start adding support for Kindle 1.25+ .kinf2018 file
|
||||
# 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya
|
||||
# 2.8 - Fix for Mac OS X Big Sur
|
||||
# 3.0 - Python 3 for calibre 5.0
|
||||
|
||||
|
||||
@@ -60,11 +61,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -108,7 +115,7 @@ def unicode_argv():
|
||||
return ["kindlekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -188,7 +195,11 @@ if iswindows:
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
MAX_PATH = 255
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
@@ -1174,8 +1185,11 @@ elif isosx:
|
||||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException("libcrypto not found")
|
||||
libcrypto = CDLL(libcrypto)
|
||||
libcrypto = '/usr/lib/libcrypto.dylib'
|
||||
try:
|
||||
libcrypto = CDLL(libcrypto)
|
||||
except Exception as e:
|
||||
raise DrmException("libcrypto not found: " % e)
|
||||
|
||||
# From OpenSSL's crypto aes header
|
||||
#
|
||||
|
||||
@@ -26,11 +26,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -71,7 +77,7 @@ def unicode_argv():
|
||||
return ["kindlepid.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
|
||||
64
DeDRM_plugin/mobidedrm.py
Normal file → Executable file
64
DeDRM_plugin/mobidedrm.py
Normal file → Executable file
@@ -94,11 +94,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -139,7 +145,7 @@ def unicode_argv():
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
@@ -191,19 +197,21 @@ def PC1(key, src, decryption=True):
|
||||
dst+=bytes([curByte])
|
||||
return dst
|
||||
|
||||
# accepts unicode returns unicode
|
||||
def checksumPid(s):
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l].encode('ascii')
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
# expects bytearray
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
@@ -282,7 +290,7 @@ class MobiBook:
|
||||
self.mobi_codepage = 1252
|
||||
self.mobi_version = -1
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
if self.magic == b'TEXtREAd':
|
||||
print("PalmDoc format book detected.")
|
||||
return
|
||||
|
||||
@@ -301,7 +309,7 @@ class MobiBook:
|
||||
# if exth region exists parse it for metadata array
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = ''
|
||||
exth = b''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 12) and (exth[:4] == b'EXTH'):
|
||||
@@ -318,17 +326,27 @@ class MobiBook:
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 405 and size == 9:
|
||||
# remove rented book flag
|
||||
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 406 and size == 16:
|
||||
# remove rental due date
|
||||
self.patchSection(0, b'\0'*8, 16 + self.mobi_length + pos + 8)
|
||||
elif type == 208:
|
||||
# remove watermark (atv:kin: stuff)
|
||||
self.patchSection(0, b'\0'*(size-8), 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except Exception as e:
|
||||
print("Cannot set meta_array: Error: {:s}".format(e.args[0]))
|
||||
|
||||
#returns unicode
|
||||
def getBookTitle(self):
|
||||
codec_map = {
|
||||
1252 : 'windows-1252',
|
||||
65001 : 'utf-8',
|
||||
}
|
||||
title = ''
|
||||
title = b''
|
||||
codec = 'windows-1252'
|
||||
if self.magic == b'BOOKMOBI':
|
||||
if 503 in self.meta_array:
|
||||
@@ -339,7 +357,7 @@ class MobiBook:
|
||||
title = self.sect[toff:tend]
|
||||
if self.mobi_codepage in codec_map.keys():
|
||||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
if title == b'':
|
||||
title = self.header[:32]
|
||||
title = title.split(b'\0')[0]
|
||||
return title.decode(codec)
|
||||
@@ -355,13 +373,15 @@ class MobiBook:
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in range(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,'')
|
||||
sval = self.meta_array.get(val,b'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
# new must be byte array
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
# new must be byte array
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
@@ -371,12 +391,12 @@ class MobiBook:
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
# pids in pidlist must be unicode, returned key is byte array, pid is unicode
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = b'\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,b'\0')
|
||||
bigpid = bigpid
|
||||
bigpid = pid.encode('utf-8').ljust(16,b'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
temp_key_sum = sum(temp_key) & 0xff
|
||||
found_key = None
|
||||
@@ -424,6 +444,7 @@ class MobiBook:
|
||||
return ".azw3"
|
||||
return ".mobi"
|
||||
|
||||
# pids in pidlist may be unicode or bytearrays or bytes
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print("Crypto Type is: {0:d}".format(crypto_type))
|
||||
@@ -440,11 +461,14 @@ class MobiBook:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
print("Warning: This is a library or rented ebook ({1}). Continuing ...".format(val406))
|
||||
#raise DrmException("Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
# print("DEBUG ==== pidlist = ", pidlist)
|
||||
for pid in pidlist:
|
||||
if isinstance(pid,(bytearray,bytes)):
|
||||
pid = pid.decode('utf-8')
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print("Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])))
|
||||
@@ -457,8 +481,8 @@ class MobiBook:
|
||||
# print("======= DEBUG good pids = ", goodpids)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
if self.magic == 'TEXtREAd':
|
||||
t1_keyvec = b'QDCVEPMU675RUBSZ'
|
||||
if self.magic == b'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
@@ -473,7 +497,7 @@ class MobiBook:
|
||||
raise DrmException("Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in {0:d} keys tried.".format(len(goodpids)))
|
||||
raise DrmException("No key found in {0:d} PIDs tried.".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, b'\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
@@ -509,6 +533,7 @@ class MobiBook:
|
||||
print("done")
|
||||
return
|
||||
|
||||
# pids in pidlist must be unicode
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException("Input File Not Found.")
|
||||
@@ -530,8 +555,7 @@ def cli_main():
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
if len(argv) == 4:
|
||||
# convert from unicode to bytearray before splitting.
|
||||
pidlist = argv[3].encode('utf-8').split(b',')
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = []
|
||||
try:
|
||||
|
||||
@@ -76,7 +76,7 @@ def load_libcrypto():
|
||||
return ob.raw
|
||||
def decrypt(self, data):
|
||||
if not data:
|
||||
return ''
|
||||
return b''
|
||||
i = 0
|
||||
result = []
|
||||
while i < len(data):
|
||||
@@ -84,6 +84,6 @@ def load_libcrypto():
|
||||
processed_block = self.desdecrypt(block)
|
||||
result.append(processed_block)
|
||||
i += 8
|
||||
return ''.join(result)
|
||||
return b''.join(result)
|
||||
|
||||
return DES
|
||||
|
||||
@@ -19,6 +19,8 @@ class DeDRM_Prefs():
|
||||
self.dedrmprefs = JSONConfig(JSON_PATH)
|
||||
|
||||
self.dedrmprefs.defaults['configured'] = False
|
||||
self.dedrmprefs.defaults['deobfuscate_fonts'] = True
|
||||
self.dedrmprefs.defaults['remove_watermarks'] = False
|
||||
self.dedrmprefs.defaults['bandnkeys'] = {}
|
||||
self.dedrmprefs.defaults['adeptkeys'] = {}
|
||||
self.dedrmprefs.defaults['ereaderkeys'] = {}
|
||||
@@ -26,6 +28,7 @@ class DeDRM_Prefs():
|
||||
self.dedrmprefs.defaults['androidkeys'] = {}
|
||||
self.dedrmprefs.defaults['pids'] = []
|
||||
self.dedrmprefs.defaults['serials'] = []
|
||||
self.dedrmprefs.defaults['lcp_passphrases'] = []
|
||||
self.dedrmprefs.defaults['adobewineprefix'] = ""
|
||||
self.dedrmprefs.defaults['kindlewineprefix'] = ""
|
||||
|
||||
@@ -47,6 +50,8 @@ class DeDRM_Prefs():
|
||||
self.dedrmprefs['pids'] = []
|
||||
if self.dedrmprefs['serials'] == []:
|
||||
self.dedrmprefs['serials'] = []
|
||||
if self.dedrmprefs['lcp_passphrases'] == []:
|
||||
self.dedrmprefs['lcp_passphrases'] = []
|
||||
|
||||
def __getitem__(self,kind = None):
|
||||
if kind is not None:
|
||||
|
||||
@@ -20,7 +20,10 @@ class SimplePrefs(object):
|
||||
self.file2key[filename] = key
|
||||
self.target = target + 'Prefs'
|
||||
if sys.platform.startswith('win'):
|
||||
import winreg
|
||||
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
|
||||
|
||||
@@ -32,11 +32,17 @@ class SafeUnbuffered:
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
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")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -77,7 +83,7 @@ def unicode_argv():
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
#global switch
|
||||
debug = False
|
||||
|
||||
@@ -93,6 +93,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
|
||||
# try finding winekeys anyway, even if above code errored
|
||||
winekeys = []
|
||||
winekey_names = []
|
||||
# get any files with extension in the output dir
|
||||
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
|
||||
for filename in files:
|
||||
@@ -104,9 +105,10 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
|
||||
else:
|
||||
new_key_value = keyfile.read()
|
||||
winekeys.append(new_key_value)
|
||||
winekey_names.append(filename)
|
||||
except:
|
||||
print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
|
||||
traceback.print_exc()
|
||||
os.remove(fpath)
|
||||
print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files"))
|
||||
return winekeys
|
||||
return winekeys, winekey_names
|
||||
|
||||
@@ -209,6 +209,7 @@ def _EndRecData(fpin):
|
||||
fpin.seek(-sizeEndCentDir, 2)
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
data = fpin.read()
|
||||
if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
|
||||
# the signature is correct and there's no comment, unpack structure
|
||||
@@ -662,7 +663,8 @@ class ZipFile:
|
||||
self.comment = b''
|
||||
|
||||
# Check if we were passed a file-like object
|
||||
if isinstance(file, str):
|
||||
# "str" is python3, "unicode" is python2
|
||||
if isinstance(file, str) or isinstance(file, unicode):
|
||||
self._filePassed = 0
|
||||
self.filename = file
|
||||
modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
|
||||
|
||||
@@ -66,21 +66,21 @@ class fixZip:
|
||||
|
||||
def uncompress(self, cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = ''
|
||||
data = b''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = ''
|
||||
cmpdata = b''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = ''
|
||||
unprocessed = b''
|
||||
return data
|
||||
|
||||
def getfiledata(self, zi):
|
||||
@@ -123,7 +123,7 @@ class fixZip:
|
||||
mimeinfo.internal_attr = 1 # text file
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldmimeinfo = self.inzip.getinfo('mimetype')
|
||||
oldmimeinfo = self.inzip.getinfo(b'mimetype')
|
||||
# copy across useful fields
|
||||
mimeinfo.date_time = oldmimeinfo.date_time
|
||||
mimeinfo.comment = oldmimeinfo.comment
|
||||
@@ -137,7 +137,7 @@ class fixZip:
|
||||
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != "mimetype" or self.ztype != 'epub':
|
||||
if zinfo.filename != b"mimetype" or self.ztype != 'epub':
|
||||
data = None
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
@@ -153,6 +153,7 @@ class fixZip:
|
||||
nzinfo.internal_attr=zinfo.internal_attr
|
||||
nzinfo.external_attr=zinfo.external_attr
|
||||
nzinfo.create_system=zinfo.create_system
|
||||
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
|
||||
self.bzf.close()
|
||||
|
||||
@@ -7,7 +7,7 @@ This plugin will remove the DRM from:
|
||||
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||
- Adobe Digital Editions (v2.0.1) PDFs
|
||||
|
||||
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 (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
|
||||
|
||||
|
||||
Installation
|
||||
@@ -31,4 +31,6 @@ If you find that the DeDRM plugin is not working for you (imported ebooks still
|
||||
- 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, 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 report at https://github.com/noDRM/DeDRM_tools/issues. Please also include information about the eBook file.
|
||||
|
||||
If you're using Apprentice Harper's original version, you can also comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or open an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues.
|
||||
43
FAQs.md
43
FAQs.md
@@ -18,23 +18,23 @@ Just download and use these tools, that's all! Uh, almost. There are a few, uh,
|
||||
|
||||
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.
|
||||
|
||||
### A Recent Change 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).
|
||||
|
||||
Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac.
|
||||
Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. (Note that this is a 32-bit application on the Mac, so will not work on Catalina and newer versions of macOS.)
|
||||
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
|
||||
|
||||
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
|
||||
* MD-5: 53F793B562F4823721AA47D7DE099869
|
||||
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
|
||||
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
|
||||
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
|
||||
|
||||
#### Kindle for Mac `KindleForMac-44182.dmg`:
|
||||
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
|
||||
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
||||
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
|
||||
* 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 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.
|
||||
|
||||
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:
|
||||
|
||||
@@ -53,11 +53,13 @@ After restarting the Kindle program any books previously downloaded in KFX forma
|
||||
#### 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.
|
||||
|
||||
It's quite possible that Amazon will update their KFX DeDRM to prevent DRM removal from KFX books again. So Remove DRM as soon as possible!
|
||||
|
||||
#### Thanks
|
||||
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?
|
||||
Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
@@ -67,8 +69,8 @@ Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin
|
||||
|
||||
# Installing the Tools
|
||||
## 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.5.3.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.5.3.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.
|
||||
### 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"
|
||||
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.
|
||||
|
||||
### 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.
|
||||
@@ -93,7 +95,7 @@ Your ebooks are stored on your computer or on your ebook reader. You need to fin
|
||||
### Macintosh
|
||||
Navigating from your home folder,
|
||||
|
||||
Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
|
||||
Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content` or `Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
|
||||
|
||||
Adobe Digital Editions ebooks are in `Documents/Digital Editions`
|
||||
|
||||
@@ -118,21 +120,20 @@ If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle S
|
||||
|
||||
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key aren’t quite in the format the tools expect. To try to fix this:
|
||||
|
||||
1. Deregister Kindle for PC(Mac) from your Amazon account.
|
||||
1. Uninstall Kindle for PC(Mac)
|
||||
1. Delete the Kindle for PC(Mac) preferences
|
||||
1. Deregister Kindle for PC/Mac from your Amazon account.
|
||||
1. Uninstall Kindle for PC/Mac
|
||||
1. Delete the Kindle for PC/Mac preferences
|
||||
* PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content`
|
||||
* Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted)
|
||||
1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links).
|
||||
1. Re-register Kindle for PC(Mac) with your Amazon account
|
||||
1. Reinstall Kindle for PC/Mac version 1.17 or earlier (see above for download links).
|
||||
1. Re-register Kindle for PC/Mac with your Amazon account
|
||||
1. Download the ebook again. Do not use the files you have downloaded previously.
|
||||
|
||||
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
|
||||
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.
|
||||
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
|
||||
|
||||
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, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
||||
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).
|
||||
|
||||
## 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.
|
||||
@@ -146,9 +147,6 @@ If you use the Kobo desktop application for Mac or PC, install the Obok plugin.
|
||||
## 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.
|
||||
|
||||
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
|
||||
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -158,7 +156,7 @@ See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCT
|
||||
# General Questions
|
||||
|
||||
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
|
||||
The tools only remove the DRM. No attempt is made to remove any personally identifying information.
|
||||
That question cannot be answered for sure. While it is easy to check if a book has DRM or not, it is very difficult to verify if all (traces of) personal information have been removed from a book. The tools attempt to remove watermarks when they are detected, but that will not be the case for all watermarks.
|
||||
|
||||
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
|
||||
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
||||
@@ -169,7 +167,7 @@ All the DRM removal tools hosted here are almost entirely scripts of one kind or
|
||||
There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files or manually rebuild them.
|
||||
|
||||
## What ebooks do these tools work on?
|
||||
The tools linked from this blog remove DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes.
|
||||
The Calibre plugin removes DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Readium LCP, Amazon, Kobo and eReader DRM schemes.
|
||||
|
||||
Note these tools do NOT ‘crack’ the DRM. They simply allow the book’s owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
|
||||
|
||||
@@ -188,11 +186,12 @@ Apple regularly change the details of their DRM and so the tools in the main too
|
||||
## 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 file for the tool you want to use.
|
||||
* If you still can’t remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, 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 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).
|
||||
|
||||
## Who wrote these scripts?
|
||||
The authors tend to identify themselves only by pseudonyms:
|
||||
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
||||
* The Readium LCP support for this plugin was created by NoDRM
|
||||
* 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 K4 Mobi tool was created by by some_updates, mdlnx and others
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '6.7.0'
|
||||
__version__ = '10.0.0'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
#####################################################################
|
||||
@@ -20,7 +20,7 @@ except NameError:
|
||||
PLUGIN_NAME = 'Obok DeDRM'
|
||||
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
|
||||
PLUGIN_VERSION_TUPLE = (6, 7, 0)
|
||||
PLUGIN_VERSION_TUPLE = (10, 0, 0)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
||||
PLUGIN_AUTHORS = 'Anon'
|
||||
|
||||
@@ -197,7 +197,7 @@ class InterfacePluginAction(InterfaceAction):
|
||||
# We will write the help file out every time, in case the user upgrades the plugin zip
|
||||
# and there is a newer help file contained within it.
|
||||
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
|
||||
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
|
||||
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME].decode('utf-8')
|
||||
with open(file_path,'w') as f:
|
||||
f.write(file_data)
|
||||
return file_path
|
||||
@@ -375,7 +375,6 @@ class InterfacePluginAction(InterfaceAction):
|
||||
#print ('Kobo library filename: {0}'.format(book.filename))
|
||||
for userkey in self.userkeys:
|
||||
print (_('Trying key: '), codecs.encode(userkey, 'hex'))
|
||||
check = True
|
||||
try:
|
||||
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
|
||||
#print ('Temp file: {0}'.format(fileout.name))
|
||||
@@ -396,8 +395,7 @@ class InterfacePluginAction(InterfaceAction):
|
||||
file = book.encryptedfiles[filename]
|
||||
contents = file.decrypt(userkey, contents)
|
||||
# Parse failures mean the key is probably wrong.
|
||||
if check:
|
||||
check = not file.check(contents)
|
||||
file.check(contents)
|
||||
zout.writestr(filename, contents)
|
||||
zout.close()
|
||||
zin.close()
|
||||
|
||||
@@ -204,7 +204,7 @@ class AddSerialDialog(QDialog):
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or self.key_name.isspace():
|
||||
errmsg = "Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
|
||||
errmsg = "Please enter an eInk Kobo Serial Number or click Cancel in the dialog."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if len(self.key_name) != 13:
|
||||
errmsg = "EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
|
||||
|
||||
@@ -42,7 +42,10 @@ class legacy_obok(object):
|
||||
pwsdid = ''
|
||||
try:
|
||||
if sys.platform.startswith('win'):
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
regkey_browser = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
|
||||
cookies = winreg.QueryValueEx(regkey_browser, 'cookies')
|
||||
bytearrays = cookies[0]
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Version 10.0.0 November 2021
|
||||
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
|
||||
# key fetch issues on some machines.
|
||||
#
|
||||
# Version 4.1.0 February 2021
|
||||
# Add detection for Kobo directory location on Linux
|
||||
#
|
||||
# Version 4.0.0 September 2020
|
||||
# Python 3.0
|
||||
#
|
||||
@@ -353,7 +360,10 @@ class KoboLibrary(object):
|
||||
|
||||
if (self.kobodir == u""):
|
||||
if sys.platform.startswith('win'):
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
if sys.getwindowsversion().major > 5:
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
@@ -365,9 +375,33 @@ class KoboLibrary(object):
|
||||
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
|
||||
elif sys.platform.startswith('darwin'):
|
||||
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
|
||||
#elif linux_path != None:
|
||||
# Probably Linux, let's get the wine prefix and path to Kobo.
|
||||
# self.kobodir = os.path.join(linux_path, "Local Settings", "Application Data", "Kobo", "Kobo Desktop Edition")
|
||||
elif sys.platform.startswith('linux'):
|
||||
|
||||
#sets ~/.config/calibre as the location to store the kobodir location info file and creates this directory if necessary
|
||||
kobodir_cache_dir = os.path.join(os.environ['HOME'], ".config", "calibre")
|
||||
if not os.path.isdir(kobodir_cache_dir):
|
||||
os.mkdir(kobodir_cache_dir)
|
||||
|
||||
#appends the name of the file we're storing the kobodir location info to the above path
|
||||
kobodir_cache_file = str(kobodir_cache_dir) + "/" + "kobo location"
|
||||
|
||||
"""if the above file does not exist, recursively searches from the root
|
||||
of the filesystem until kobodir is found and stores the location of kobodir
|
||||
in that file so this loop can be skipped in the future"""
|
||||
original_stdout = sys.stdout
|
||||
if not os.path.isfile(kobodir_cache_file):
|
||||
for root, dirs, files in os.walk('/'):
|
||||
for file in files:
|
||||
if file == 'Kobo.sqlite':
|
||||
kobo_linux_path = str(root)
|
||||
with open(kobodir_cache_file, 'w') as f:
|
||||
sys.stdout = f
|
||||
print(kobo_linux_path, end='')
|
||||
sys.stdout = original_stdout
|
||||
|
||||
f = open(kobodir_cache_file, 'r' )
|
||||
self.kobodir = f.read()
|
||||
|
||||
# desktop versions use Kobo.sqlite
|
||||
kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
|
||||
# check for existence of file
|
||||
@@ -443,15 +477,22 @@ class KoboLibrary(object):
|
||||
"""The list of all MAC addresses on this machine."""
|
||||
macaddrs = []
|
||||
if sys.platform.startswith('win'):
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
|
||||
for line in output:
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
try:
|
||||
output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
|
||||
for line in output:
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
except:
|
||||
output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
|
||||
for line in output:
|
||||
m = c.search(line)
|
||||
if m:
|
||||
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||
elif sys.platform.startswith('darwin'):
|
||||
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
|
||||
output = subprocess.check_output('/sbin/ifconfig -a', shell=True, encoding='utf-8')
|
||||
matches = c.findall(output)
|
||||
for m in matches:
|
||||
# print "m:{0}".format(m[0])
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<body>
|
||||
|
||||
<h1>Obok DeDRM Plugin</h1>
|
||||
<h3>(version 3.1.3)</h3>
|
||||
<h3>(version 10.0.0)</h3>
|
||||
|
||||
<h3>Installation:</h3>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<h3>Troubleshooting:</h3>
|
||||
|
||||
<p >If you find that it’s not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
|
||||
<p >If you find that it’s not working for you, you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
|
||||
|
||||
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at Apprentice Alf's blog.</p>
|
||||
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
|
||||
"PO-Revision-Date: 2020-02-02 09:18+0100\n"
|
||||
"PO-Revision-Date: 2021-01-19 12:20+0100\n"
|
||||
"Language: sv\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -16,14 +16,14 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
"X-Generator: Poedit 2.4.2\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
|
||||
msgid ""
|
||||
"<p>No books found in Kobo Library\n"
|
||||
"Are you sure it's installed\\configured\\synchronized?"
|
||||
msgstr ""
|
||||
"<p>Inga böcker finns i Kobo-bibliotek\n"
|
||||
"<p>Inga böcker hittades i Kobo-bibliotek\n"
|
||||
"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
|
||||
@@ -36,7 +36,7 @@ msgstr "Problem med att hämta nycklar med nyare obok-metod."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
|
||||
msgid "Found {0} possible keys to try."
|
||||
msgstr "Hittade {0} möjliga nycklar att pröva med."
|
||||
msgstr "Hittade {0} möjliga nycklar att försöka med."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
|
||||
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
|
||||
@@ -46,7 +46,7 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
|
||||
msgid "{} - Decryption canceled by user."
|
||||
msgstr "{} - Dekryptering avbryts av användaren."
|
||||
msgstr "{} - Dekryptering avbröts av användaren."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
|
||||
msgid "{} - \"Add books\" canceled by user."
|
||||
@@ -87,14 +87,14 @@ msgstr "dubblett upptäcktes"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
|
||||
msgid "{0} - Successfully added EPUB format to existing {1}"
|
||||
msgstr "{0} - Lade till EPUB-format till befintliga {1}"
|
||||
msgstr "{0} - EPUB-formatet har lagts till i befintliga {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
|
||||
msgid ""
|
||||
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
|
||||
msgstr ""
|
||||
"{0} - Fel vid tillägg av EPUB-format till befintligt {1}. Det här borde inte "
|
||||
"hända."
|
||||
"{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
|
||||
"verkligen inte hända."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
|
||||
msgid "{} - \"Insert formats\" canceled by user."
|
||||
@@ -103,44 +103,44 @@ msgstr "{} - \"Infoga format\" avbröts av användaren."
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
|
||||
msgid ""
|
||||
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
|
||||
msgstr "<p><b>{0}</b> EPUB{2} lades till bibliotek.<br /><br /><b>{1}</b> "
|
||||
msgstr "<p><b>{0}</b> EPUB{2} har lagts till i bibliotek.<br /><br /><b>{1}</b> "
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
|
||||
msgid ""
|
||||
"not added because books with the same title/author were detected.<br /><br /"
|
||||
">Would you like to try and add the EPUB format{0}"
|
||||
msgstr ""
|
||||
"inte tillagd eftersom böcker med samma titel/författare upptäcktes.<br/><br /"
|
||||
">Vill du försöka lägga till EPUB-formatet{0}"
|
||||
"lades inte till eftersom böcker med samma titel/författare upptäcktes.<br/"
|
||||
"><br />Vill du försöka lägga till EPUB-formatet{0}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
|
||||
msgid ""
|
||||
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
|
||||
"overwritten."
|
||||
msgstr ""
|
||||
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er "
|
||||
"kommer att skrivas över."
|
||||
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
|
||||
"att skrivas över."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
|
||||
msgid ""
|
||||
"{0} -- not added because of {1} in your library.\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
"{0} -- inte tillagd på grund av {1} i ditt bibliotek.\n"
|
||||
"{0} -- lades inte till på grund av {1} i ditt bibliotek.\n"
|
||||
"\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
|
||||
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
|
||||
msgstr ""
|
||||
"<p><b>{0}</b> -- inte tillagd på grund av {1} i ditt bibliotek.<br /><br />"
|
||||
"<p><b>{0}</b> -- lades inte till på grund av {1} i ditt bibliotek.<br /><br />"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
|
||||
msgid ""
|
||||
"Would you like to try and add the EPUB format to an available calibre "
|
||||
"duplicate?<br /><br />"
|
||||
msgstr ""
|
||||
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-"
|
||||
"dubblett?<br /><br />"
|
||||
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
|
||||
"<br /><br />"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
|
||||
msgid "NOTE: no pre-existing EPUB will be overwritten."
|
||||
@@ -148,23 +148,23 @@ msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
|
||||
msgid "Trying key: "
|
||||
msgstr "Prövar nyckel: "
|
||||
msgstr "Försöker med nyckel: "
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
|
||||
msgid "Decryption failed, trying next key."
|
||||
msgstr "Det gick inte att dekryptera, prövar nästa nyckel."
|
||||
msgstr "Dekryptering misslyckades, försöker med nästa nyckel."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
|
||||
msgid "Unknown Error decrypting, trying next key.."
|
||||
msgstr "Okänt fel dekryptering, prövar nästa nyckel.."
|
||||
msgstr "Okänt fel vid dekryptering, försöker med nästa nyckel.."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
|
||||
msgid ""
|
||||
"<p>All selected Kobo books added as new calibre books or inserted into "
|
||||
"existing calibre ebooks.<br /><br />No issues."
|
||||
msgstr ""
|
||||
"<p>Alla valda Kobo-böcker läggs till som nya calibre-böcker eller infogas i "
|
||||
"befintliga calibre-e-böcker.<br /><br />Inga problem."
|
||||
"<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
|
||||
"i befintliga calibre-e-böcker.<br /><br />Inga problem."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
|
||||
msgid "<p>{0} successfully added."
|
||||
@@ -192,15 +192,14 @@ msgstr "<p><b>Nya böcker skapade:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
|
||||
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
|
||||
msgstr "<p><b>Dubbletter som inte tillsattes:</b> {}</p>\n"
|
||||
msgstr "<p><b>Dubbletter som inte har lagts till:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
|
||||
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
|
||||
msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
|
||||
msgid ""
|
||||
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
|
||||
msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
|
||||
msgstr ""
|
||||
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
|
||||
|
||||
@@ -208,8 +207,7 @@ msgstr ""
|
||||
msgid ""
|
||||
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
|
||||
msgstr ""
|
||||
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br /"
|
||||
">\n"
|
||||
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
|
||||
msgid ""
|
||||
@@ -221,7 +219,7 @@ msgstr ""
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
|
||||
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
|
||||
msgstr "<p><b>Format-import avbröts av användaren:</b> {}</p>\n"
|
||||
msgstr "<p><b>Format-importen avbröts av användaren:</b> {}</p>\n"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
|
||||
msgid "Unknown Book Title"
|
||||
@@ -233,11 +231,11 @@ msgstr "den kunde inte dekrypteras."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
|
||||
msgid ""
|
||||
"user CHOSE not to insert the new EPUB format, or all existing calibre "
|
||||
"entries HAD an EPUB format already."
|
||||
"user CHOSE not to insert the new EPUB format, or all existing calibre entries "
|
||||
"HAD an EPUB format already."
|
||||
msgstr ""
|
||||
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla "
|
||||
"befintliga calibre-poster hade redan ett EPUB-format."
|
||||
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
|
||||
"calibre-poster hade redan ett EPUB-format."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
|
||||
msgid "of unknown reasons. Gosh I'm embarrassed!"
|
||||
@@ -245,7 +243,7 @@ msgstr "av okända skäl. Jag skäms!"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
|
||||
msgid "<p>{0} not added because {1}"
|
||||
msgstr "<p>{0} inte tillagd eftersom {1}"
|
||||
msgstr "<p>{0} lades inte till på grund av {1}"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
|
||||
msgid "Help"
|
||||
@@ -254,31 +252,31 @@ msgstr "Hjälp"
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
|
||||
msgid "Restart required"
|
||||
msgstr "Omstart krävs"
|
||||
msgstr "Kräver omstart"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
|
||||
msgid ""
|
||||
"Title image not found - you must restart Calibre before using this plugin!"
|
||||
msgstr ""
|
||||
"Titelbild hittades inte - du måste starta calibre innan du använder denna "
|
||||
"Titelbild hittades inte - du måste starta om calibre innan du använder denna "
|
||||
"insticksmodul!"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
|
||||
msgid "Undefined"
|
||||
msgstr "Obestämd"
|
||||
msgstr "Odefinierad"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
|
||||
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
|
||||
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-böcker?"
|
||||
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-poster?"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
|
||||
msgid ""
|
||||
"<p>Default behavior when duplicates are detected. None of the choices will "
|
||||
"cause calibre ebooks to be overwritten"
|
||||
msgstr ""
|
||||
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer "
|
||||
"att orsaka calibre-e-böcker att skrivas över"
|
||||
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
|
||||
"orsaka att calibre-e-böcker skrivs över"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||
msgid "Ask"
|
||||
@@ -323,7 +321,7 @@ msgstr "Välj alla böcker med DRM."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
|
||||
msgid "All DRM free"
|
||||
msgstr "Alla DRM fria"
|
||||
msgstr "Alla utan DRM"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
|
||||
msgid "Select all books without DRM."
|
||||
@@ -355,12 +353,12 @@ msgstr "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
|
||||
msgid "AES improper key used"
|
||||
msgstr "AES felaktig nyckel används"
|
||||
msgstr "Felaktig AES-nyckel används"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
|
||||
msgid "Failed to initialize AES key"
|
||||
msgstr "Det gick inte att initiera AES-nyckel"
|
||||
msgstr "Misslyckades med att initiera AES-nyckel"
|
||||
|
||||
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
|
||||
msgid "AES decryption failed"
|
||||
msgstr "AES dekryptering misslyckades"
|
||||
msgstr "AES-dekryptering misslyckades"
|
||||
|
||||
@@ -129,7 +129,10 @@ if iswindows:
|
||||
c_long, c_ulong
|
||||
|
||||
from ctypes.wintypes import LPVOID, DWORD, BOOL
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes.util import find_library
|
||||
|
||||
@@ -98,7 +98,10 @@ def getNookLogFiles():
|
||||
logFiles = []
|
||||
found = False
|
||||
if iswindows:
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
# some 64 bit machines do not have the proper registry key for some reason
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
|
||||
@@ -177,7 +177,10 @@ if iswindows:
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
MAX_PATH = 255
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
|
||||
@@ -346,7 +346,10 @@ class KoboLibrary(object):
|
||||
if (self.kobodir == u""):
|
||||
# step 4. we haven't found a device with serials, so try desktop apps
|
||||
if sys.platform.startswith('win'):
|
||||
import winreg
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
if sys.getwindowsversion().major > 5:
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
|
||||
@@ -2290,10 +2290,13 @@ class PDFDocument(object):
|
||||
import win32api
|
||||
import win32security
|
||||
import win32file
|
||||
import winreg
|
||||
except:
|
||||
raise ADEPTError('PyWin Extension (Win32API module) needed.\n'+\
|
||||
'Download from http://sourceforge.net/projects/pywin32/files/ ')
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
try:
|
||||
v0 = win32api.GetVolumeInformation('C:\\')
|
||||
v1 = win32api.GetSystemInfo()[6]
|
||||
|
||||
24
README.md
24
README.md
@@ -1,24 +1,30 @@
|
||||
# DeDRM_tools
|
||||
DeDRM tools for ebooks
|
||||
# [Guide] How to remove DRM
|
||||
Refer to [Wiki Page](https://github.com/apprenticeharper/DeDRM_tools/wiki/Exactly-how-to-remove-DRM)
|
||||
# calibre 5.0
|
||||
|
||||
Yes, we know that the plugin doesn't work with calibre 5.0. It will eventually, but there's a lot of code to be translated that does complicated stuff with bits and bytes that must be converted by hand. It'll take a while.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
This is a repository of 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.)
|
||||
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.
|
||||
|
||||
Mostly it tracks the tools released by Apprentice Alf, athough it also includes the individual tools and their histories from before Alf had a blog.
|
||||
# Original README from Apprentice Harper
|
||||
|
||||
Users should download the latest zip archive.
|
||||
Developers might be interested in forking the repository, as it contains unzipped versions of those tools that are zipped to make the changes over time easier to follow.
|
||||
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 DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
|
||||
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 calibe 4.x or earlier should use release 6.8.x of the tools.
|
||||
|
||||
For the latest Amazon KFX format, users of the calibre plugin should also install the KFX Input plugin from the standard calibre plugin menu. It's also available from the MobileRead thread here: https://www.mobileread.com/forums/showthread.php?t=291290
|
||||
|
||||
Note that Amazon changes the DRM for KFX files frequently. What works for KFX today might not work tomorrow.
|
||||
|
||||
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. You can't load the whole archive into calibre.
|
||||
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.
|
||||
|
||||
My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools.
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ The tools are provided in the form of plugins for calibre. Calibre is an open so
|
||||
|
||||
DeDRM plugin for calibre (Mac OS X, 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.
|
||||
|
||||
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.
|
||||
|
||||
Once installed and configured, you can simply add a DRM book to calibre and a DRM-free version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time. If you have already imported DRMed books you'll need to remove the books from calibre and re-import them.
|
||||
@@ -53,6 +55,7 @@ It may be possible to use the plugins on a Linux system, but no support is given
|
||||
Credits
|
||||
-------
|
||||
The original inept and ignoble scripts were by i♥cabbages
|
||||
The original Readium LCP DRM removal by NoDRM
|
||||
The original mobidedrm and erdr2pml scripts were by The Dark Reverser
|
||||
The original topaz DRM removal script was by CMBDTC
|
||||
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
|
||||
|
||||
Reference in New Issue
Block a user