Compare commits

...

71 Commits

Author SHA1 Message Date
NoDRM
2edde54c44 Fixes a bug that sometimes caused the plugin to fail 2021-11-19 12:44:10 +01:00
NoDRM
a44b50d1d8 LCP support 2021-11-17 21:53:24 +01:00
NoDRM
05e0d0bedb Make CI auto-package the plugin 2021-11-17 21:38:08 +01:00
NoDRM
1b391da815 Add some more watermark removal code 2021-11-17 16:17:30 +01:00
Derek Tracy
1545d76803 Support Python 2.7 and Python 3 winreg imports on Windows 2021-11-16 21:22:13 +01:00
NoDRM
d9353bdd93 Obok plugin cleanup 2021-11-16 21:22:09 +01:00
NoDRM
5d10420422 Fix font deobfuscation for Python 2 2021-11-16 20:09:24 +01:00
NoDRM
f20bede242 Auto-import keys from DeACSM plugin 2021-11-16 17:14:03 +01:00
NoDRM
39f8595139 Remove CDP watermark from EPUBs 2021-11-16 15:23:54 +01:00
NoDRM
9c41716e5e Add B&N PDF DeDRM (untested), match UUID for Adobe PDFs 2021-11-16 11:48:53 +01:00
NoDRM
b4c0e33b8b Fix ADE key import through plugin settings 2021-11-16 11:21:03 +01:00
NoDRM
90910ab106 Add back Python2 support (ADEPT) 2021-11-16 11:09:03 +01:00
NoDRM
88dd1350c0 Add useful error message for the new, uncracked ADEPT DRM 2021-11-15 19:51:36 +01:00
NoDRM
40a8e4360b No longer break obfuscated fonts on DRM removal 2021-11-15 18:38:34 +01:00
NoDRM
17ccc4d1b9 Add IETF and Adobe font deobfuscation code 2021-11-15 17:59:48 +01:00
John Belmonte
30425c1ec8 FAQ: note that Kindle 1.17 on Mac is 32-bit 2021-11-15 14:51:33 +01:00
Aldo Bleeker
77dcc462aa Fix for decryption check 2021-11-15 14:44:20 +01:00
NoDRM
be57bcca7d Enable issue forms 2021-11-15 14:39:48 +01:00
NoDRM
4a58f7017c Add old B&N algorihm (optional) just in case it's needed 2021-11-15 14:30:32 +01:00
NoDRM
eae512da8c Remove library flag from MOBI book 2021-11-15 14:14:36 +01:00
matimatik
7058fbeb98 Added a code to remove Kindle watermark.
f3fbc3573e
2021-11-15 14:06:09 +01:00
NoDRM
8cd3523a17 Remove library book block 2021-11-15 13:59:20 +01:00
NoDRM
cc17d9cc59 Improve key detection for PDFs, too 2021-11-15 13:38:39 +01:00
NoDRM
969fe52e13 Improve key detection 2021-11-15 11:59:56 +01:00
NoDRM
95fc924d1a Update Readme 2021-11-15 11:00:06 +01:00
NoDRM
0313088c15 Make keys fit into listbox 2021-11-15 10:56:26 +01:00
NoDRM
066e613cee Add UUID to adobekey DER file names 2021-11-15 10:47:09 +01:00
journeyman88
14947cd10c Update obok.py
Changed MAC address fetching code to address possibile regression
2021-11-15 09:57:11 +01:00
NoDRM
0005bba3c3 Remove broken CI 2021-11-15 09:43:12 +01:00
NoDRM
8e10b090a2 More PDF fixes 2021-11-15 08:40:18 +01:00
Olaf Fricke
007a8e8a15 Issue 1635: Decypting PDF ebboks fixed 2021-11-15 08:39:02 +01:00
Apprentice Harper
73af5d355d whitespace and some unicode/bytes
Minor changes.
2021-04-11 16:43:16 +01:00
Apprentice Harper
45a1a64db5 Update version and FAQs
Version 7.2.0 with all the latest pull requests, including on for the latest KFX encryption.
2021-04-11 15:28:33 +01:00
Apprentice Harper
bc1c3c2197 Merge pull request #1490 from llrosy798/patch-1
update voucher envelope obfuscation table
2021-04-11 15:14:08 +01:00
Apprentice Harper
79cfddfbee Merge pull request #1650 from romanbsd/bugfix
Python 3.x fix
2021-04-11 15:10:32 +01:00
Apprentice Harper
aa41bba68c Merge pull request #1615 from ableeker/python3
Python 3 fix
2021-04-11 15:09:39 +01:00
Apprentice Harper
86a90117e5 Merge pull request #1586 from raiden64/master
Fix in keyfetch for obok on MacOS
2021-04-11 15:07:17 +01:00
Apprentice Harper
874a6b8de9 Merge pull request #1575 from journeyman88/master
Fix in keyfetch for obok on win10
2021-04-11 14:05:09 +01:00
Apprentice Harper
01c654cb68 Merge pull request #1560 from Threak/master
Try new openssl library name
2021-04-11 14:04:04 +01:00
Apprentice Harper
5bc28623cb Merge pull request #1546 from mkb79/master
Enhance parsing DrmIon files
2021-04-11 14:00:21 +01:00
Apprentice Harper
c1d7fcbb7f Merge pull request #1545 from lejando/patch-1
Update FAQs.md. Thanks, lejando.
2021-04-11 13:57:29 +01:00
Apprentice Harper
45eefd6c80 Merge pull request #1539 from josdion/master
Preserve filename encoding flag when fixing epub archive
2021-04-11 13:56:19 +01:00
Roman Shterenzon
33e37eb375 Python 3.x fix 2021-04-08 16:46:14 +03:00
Aldo Bleeker
4229b8ff85 Another Python 3 fix 2021-04-05 17:06:24 +02:00
Aldo Bleeker
91e4645315 Another Python 3 fix 2021-04-05 12:16:02 +02:00
Aldo Bleeker
425d8af73e Python 3 fix 2021-03-22 19:24:34 +01:00
raiden64
0ce86fa8db Fix in keyfetch for obok on MacOS 2021-03-05 22:54:53 +01:00
journeyman88
ecc7db09a9 Fix in keyfetch for obok on win10
According to calibre debug the ipconfig command returned some invalid utf-8 characters (I think is maybe an issue due to the Python2 switch-off as the 4.x version worked fine).
To solve this I've changed the external call and modified the regex to match both the output of "ipconfig" and that of "wmic".
2021-03-01 21:15:20 +01:00
Threak
d7ddc2ab93 Try new openssl library name 2021-02-26 18:50:10 +01:00
mkb79
fd51422a36 Enhance parsing DrmIon files
Adding support for parsing plaintext in DrmIon files.

This is needed by my kindle project. When downloading an ebook with my package it gives me a metadata file wich is DrmIon encoded. This file containes plaintext instead of encrypted pages.
2021-02-22 14:16:15 +01:00
lejando
cb36ca1b0d Update FAQs.md
Removed space from Mac and Win and period from Mac SHA-256 Hashes, which prevent automatic comparison.
2021-02-22 08:51:00 +01:00
Apprentice Harper
76a47e0dd0 Version number update
Update to 7.1.0 for a full release
2021-02-21 14:35:49 +00:00
Apprentice Harper
70a754fb46 Merge pull request #1529 from ableeker/python3
Fix for Python 3
2021-02-21 14:19:59 +00:00
josdion
ffd79d5fe4 Preserve filename encoding flag when fixing epub archive 2021-02-18 12:38:19 +02:00
Aldo Bleeker
21a7b13524 Fix for Python 3 2021-02-14 12:50:55 +01:00
Apprentice Harper
52bdbe95c9 Merge pull request #1522 from lkcv/patch-1
Add detection for Kobo directory location on Linux
2021-02-14 08:56:58 +00:00
Apprentice Harper
495dda3809 Merge pull request #1502 from ableeker/python3
Fix for broken book keys
2021-02-14 08:55:56 +00:00
Apprentice Harper
52e83922c0 Merge pull request #1499 from xxyzz/kfx
encode serialnum before returning it, close #1479
2021-02-14 08:50:26 +00:00
lkcv
6cbc5285cb Update obok.py 2021-02-07 21:21:03 -05:00
Aldo Bleeker
33b9630ca5 Fix for broken book keys 2021-01-28 13:06:59 +01:00
xxyzz
9346f86f73 encode serialnum before returning it, close #1479 2021-01-27 14:31:05 +08:00
Apprentice Harper
8d2d6627cf Merge pull request #1482 from 2weak2live/master
Fix python3 encoding problem in voucher decryption
2021-01-23 14:32:43 +00:00
Apprentice Harper
6f198b247c Merge pull request #1481 from icaroscherma/patch-1
[Tetrachroma FileOpen] Fixes Python 2.7 import issue, not linked to pywin
2021-01-23 14:30:21 +00:00
Apprentice Harper
9fb95eff41 Merge pull request #1491 from jony0008/master
Update sv
2021-01-23 14:29:56 +00:00
llrosy798
0b2b81fd23 fix previous bug 2021-01-21 23:48:04 +09:00
llrosy798
63aecc598f update secret table 2021-01-21 23:46:03 +09:00
llrosy798
51c8be6baf fill unknown symbols in known catalog 2021-01-21 23:41:22 +09:00
Jony
7aab8a3711 Update sv 2021-01-20 12:01:00 +01:00
2Weak2Live
2789cee331 Fix python3 encoding problem in voucher decryption 2021-01-13 22:44:11 -05:00
Ícaro R. Scherma
823704cf36 Fixes Python 2.7 import issue, not linked to pywin 2021-01-13 16:44:16 -08:00
Apprentice Harper
a7974f0f14 Update ineptpdf.py
integer division, and version
2021-01-03 16:11:02 +00:00
58 changed files with 2248 additions and 649 deletions

View File

@@ -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
View 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.
```

View File

@@ -1,39 +0,0 @@
name: Python code format
on:
push:
branches: master
jobs:
Format:
if: "contains(github.event.head_commit.message, '!format')"
runs-on: ubuntu-20.04
strategy:
fail-fast: false
steps:
- uses: actions/checkout@main
- name: Set up Python
uses: actions/setup-python@main
with:
python-version: 3.x
- uses: actions/cache@main
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-format
restore-keys: |
${{ runner.os }}-pip-format
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install autopep8 pycodestyle
- name: Format by autopep8 then Push
env:
GIT_EMAIL: github-actions[bot]@users.noreply.github.com
GIT_ACTOR: github-actions[bot]
run: |
export HASH_SHORT=$(git rev-parse --short HEAD)
git checkout -b format--${HASH_SHORT}
git config --global user.email $GIT_EMAIL
git config --global user.name $GIT_ACTOR
python -m autopep8 --in-place --aggressive --aggressive --experimental -r ./
git add -A
git commit -m 'Format by autopep8' -m From: -m $(git rev-parse HEAD)
git push --set-upstream origin format--${HASH_SHORT}

View File

@@ -1,26 +0,0 @@
name: Python code review
on: [push, pull_request]
jobs:
Test:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
steps:
- uses: actions/checkout@main
- name: Set up Python
uses: actions/setup-python@main
with:
python-version: 3.x
- uses: actions/cache@main
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-lint
restore-keys: |
${{ runner.os }}-pip-lint
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
- name: Lint with flake8
run: |
python -m flake8 . --builtins=_,I --ignore=E501 --count --benchmark --show-source --statistics

19
.github/workflows/main.yml vendored Normal file
View 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.1
- name: Upload
uses: actions/upload-artifact@v2
with:
name: plugin
path: |
DeDRM_tools_*.zip

23
CHANGELOG.md Normal file
View 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.

View File

@@ -17,15 +17,19 @@ p {margin-top: 0}
<body> <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>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> <h3>Installation</h3>
<p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p> <p>You have obviously managed to install the plugin, as otherwise you wouldnt 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> <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> <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> <h3>Credits:</h3>
<ul> <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>The Dark Reverser for the Mobipocket and eReader scripts</li>
<li>i♥cabbages for the Adobe Digital Editions scripts</li> <li>i♥cabbages for the Adobe Digital Editions scripts</li>
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li> <li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
<li>CMBDTC for Amazon Topaz DRM removal 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>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, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
<li>some_updates for the DeDRM all-in-one Python tool</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> <li>And probably many more.</li>
</ul> </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 Harperss 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 Harperss 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> <h2>Linux Systems Only</h2>
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3> <h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>

View File

@@ -35,7 +35,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p> <p>On the right-hand side of the plugins 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 thats what you truly mean to do. Once gone, its 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> </body>

View 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 plugins 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 plugins 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 thats what you truly mean to do. Once gone, its 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>

View File

@@ -3,9 +3,10 @@
# __init__.py for DeDRM_plugin # __init__.py for DeDRM_plugin
# Copyright © 2008-2020 Apprentice Harper et al. # Copyright © 2008-2020 Apprentice Harper et al.
# Copyright © 2021 NoDRM
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '7.0.2' __version__ = '10.0.1'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -73,6 +74,12 @@ __docformat__ = 'restructuredtext en'
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who contributed # 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.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.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.
# 10.0.1 - Fixes a bug in the watermark code.
""" """
Decrypt DRMed ebooks. Decrypt DRMed ebooks.
@@ -85,11 +92,10 @@ PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
import codecs import codecs
import sys, os, re import sys, os
import time import time
import zipfile
import traceback import traceback
from zipfile import ZipFile
class DeDRMError(Exception): class DeDRMError(Exception):
pass pass
@@ -110,24 +116,28 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
try: try:
self.stream.buffer.write(data) buffer = getattr(self.stream, 'buffer', self.stream)
self.stream.buffer.flush() # self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except: except:
# We can do nothing if a write fails # We can do nothing if a write fails
pass raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
class DeDRM(FileTypePlugin): class DeDRM(FileTypePlugin):
name = PLUGIN_NAME 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'] 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 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']) file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip'])
on_import = True on_import = True
on_preprocess = True on_preprocess = True
@@ -146,6 +156,7 @@ class DeDRM(FileTypePlugin):
The extraction only happens once per version of the plugin The extraction only happens once per version of the plugin
Also perform upgrade of preferences once per version Also perform upgrade of preferences once per version
""" """
try: try:
self.pluginsdir = os.path.join(config_dir,"plugins") self.pluginsdir = os.path.join(config_dir,"plugins")
if not os.path.exists(self.pluginsdir): if not os.path.exists(self.pluginsdir):
@@ -195,10 +206,70 @@ class DeDRM(FileTypePlugin):
traceback.print_exc() traceback.print_exc()
raise 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.
postProcessStart = time.time()
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 (Amazon or LemonInk) from the OPF file
path_to_ebook = watermark.removeOPFwatermarks(self, path_to_ebook) or path_to_ebook
# Remove watermarks (Adobe or LemonInk) from all HTML and XHTML files
path_to_ebook = watermark.removeHTMLwatermarks(self, path_to_ebook) or path_to_ebook
postProcessEnd = time.time()
print("{0} v{1}: Post-processing took {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, postProcessEnd-postProcessStart))
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): def ePubDecrypt(self,path_to_ebook):
# Create a TemporaryPersistent file to work with. # Create a TemporaryPersistent file to work with.
# Check original epub archive for zip errors. # Check original epub archive for zip errors.
import calibre_plugins.dedrm.zipfix import calibre_plugins.dedrm.zipfix as zipfix
inf = self.temporary_file(".epub") inf = self.temporary_file(".epub")
try: try:
@@ -207,12 +278,28 @@ class DeDRM(FileTypePlugin):
fr.fix() fr.fix()
except Exception as e: except Exception as e:
print("{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])) 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 the decryption keys
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs
dedrmprefs = prefs.DeDRM_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 the Barnes & Noble ePub handler
import calibre_plugins.dedrm.ignobleepub as ignobleepub import calibre_plugins.dedrm.ignobleepub as ignobleepub
@@ -240,7 +327,7 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # 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)) 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))
@@ -299,7 +386,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)) 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() traceback.print_exc()
# Return the modified PersistentTemporary file to calibre. # 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)) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception as e: except Exception as e:
@@ -312,7 +399,45 @@ class DeDRM(FileTypePlugin):
import calibre_plugins.dedrm.ineptepub as ineptepub import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name): 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). # Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
@@ -323,6 +448,9 @@ class DeDRM(FileTypePlugin):
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptepub.decryptBook(userkey, inf.name, of.name) 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: except:
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
@@ -337,7 +465,7 @@ class DeDRM(FileTypePlugin):
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # 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)) 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)) 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))
@@ -351,23 +479,45 @@ class DeDRM(FileTypePlugin):
if iswindows or isosx: if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys() defaultkeys, defaultnames = adeptkeys()
else: # linux else: # linux
from .wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,"adobekey.py") 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: except:
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) 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() traceback.print_exc()
self.default_key = "" self.default_key = ""
newkeys = [] newkeys = []
newnames = []
idx = 0
for keyvalue in defaultkeys: for keyvalue in defaultkeys:
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values(): if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue) 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: if len(newkeys) > 0:
try: try:
@@ -390,7 +540,7 @@ class DeDRM(FileTypePlugin):
# Store the new successful key in the defaults # Store the new successful key in the defaults
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: 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() dedrmprefs.writeprefs()
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
@@ -398,7 +548,7 @@ class DeDRM(FileTypePlugin):
traceback.print_exc() 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)) 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 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)) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception as e: except Exception as e:
@@ -411,25 +561,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)) 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 # 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))) 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): def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs 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() 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(): for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = codecs.decode(userkeyhex,'hex') userkey = codecs.decode(userkeyhex,'hex')
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname)) print("{0} v{1}: Trying encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(".pdf") of = self.temporary_file(".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) 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: except:
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
@@ -440,6 +646,7 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # 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 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)) 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))
@@ -454,23 +661,44 @@ class DeDRM(FileTypePlugin):
if iswindows or isosx: if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys() defaultkeys, defaultnames = adeptkeys()
else: # linux else: # linux
from .wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,"adobekey.py") 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: except:
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) 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() traceback.print_exc()
self.default_key = "" self.default_key = ""
newkeys = [] newkeys = []
newnames = []
idx = 0
for keyvalue in defaultkeys: for keyvalue in defaultkeys:
if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values(): if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue) 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: if len(newkeys) > 0:
try: try:
@@ -493,7 +721,7 @@ class DeDRM(FileTypePlugin):
# Store the new successful key in the defaults # Store the new successful key in the defaults
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',codecs.encode(keyvalue,'hex')) dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey,'hex').decode('ascii'))
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
@@ -506,6 +734,36 @@ class DeDRM(FileTypePlugin):
except Exception as e: except Exception as e:
pass 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. # 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)) 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)) 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))

View File

@@ -51,11 +51,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -99,7 +105,7 @@ def unicode_argv():
return ["adobekey.py"] return ["adobekey.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class ADEPTError(Exception):
pass pass
@@ -111,11 +117,16 @@ if iswindows:
c_long, c_ulong c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL from ctypes.wintypes import LPVOID, DWORD, BOOL
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes.util import find_library 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: if libcrypto is None:
raise ADEPTError('libcrypto not found') raise ADEPTError('libcrypto not found')
libcrypto = CDLL(libcrypto) libcrypto = CDLL(libcrypto)
@@ -360,6 +371,7 @@ if iswindows:
keykey = CryptUnprotectData(device, entropy) keykey = CryptUnprotectData(device, entropy)
userkey = None userkey = None
keys = [] keys = []
names = []
try: try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError: except WindowsError:
@@ -372,12 +384,15 @@ if iswindows:
ktype = winreg.QueryValueEx(plkparent, None)[0] ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype != 'credentials': if ktype != 'credentials':
continue continue
uuid_name = "Unknown"
for j in range(0, 16): for j in range(0, 16):
try: try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError: except WindowsError:
break break
ktype = winreg.QueryValueEx(plkkey, None)[0] ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype == 'user':
uuid_name = winreg.QueryValueEx(plkkey, 'value')[0]
if ktype != 'privateLicenseKey': if ktype != 'privateLicenseKey':
continue continue
userkey = winreg.QueryValueEx(plkkey, 'value')[0] userkey = winreg.QueryValueEx(plkkey, 'value')[0]
@@ -385,12 +400,13 @@ if iswindows:
aes = AES(keykey) aes = AES(keykey)
userkey = aes.decrypt(userkey) userkey = aes.decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1:])] userkey = userkey[26:-ord(userkey[-1:])]
#print "found key:",userkey.encode('hex') # print ("found " + uuid_name + " key: " + str(userkey))
keys.append(userkey) keys.append(userkey)
names.append(uuid_name[9:])
if len(keys) == 0: if len(keys) == 0:
raise ADEPTError('Could not locate privateLicenseKey') raise ADEPTError('Could not locate privateLicenseKey')
print("Found {0:d} keys".format(len(keys))) print("Found {0:d} keys".format(len(keys)))
return keys return keys, names
elif isosx: elif isosx:
@@ -429,19 +445,25 @@ elif isosx:
tree = etree.parse(actpath) tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
userkey = tree.findtext(expr) userkey = tree.findtext(expr)
userUUID = "Unknown"
try:
userUUID = tree.findtext(exprUUID)
except:
pass
userkey = b64decode(userkey) userkey = b64decode(userkey)
userkey = userkey[26:] userkey = userkey[26:]
return [userkey] return [userkey], [userUUID[9:]]
else: else:
def adeptkeys(): def adeptkeys():
raise ADEPTError("This script only supports Windows and Mac OS X.") raise ADEPTError("This script only supports Windows and Mac OS X.")
return [] return [], []
# interface for Python DeDRM # interface for Python DeDRM
def getkey(outpath): def getkey(outpath):
keys = adeptkeys() keys, names = adeptkeys()
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
@@ -450,15 +472,17 @@ def getkey(outpath):
print("Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
with open(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
print("Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
name_index += 1
return True return True
return False return False
@@ -504,7 +528,7 @@ def cli_main():
# make sure the outpath is the # make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath)) outpath = os.path.realpath(os.path.normpath(outpath))
keys = adeptkeys() keys, names = adeptkeys()
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
@@ -513,15 +537,17 @@ def cli_main():
print("Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
with open(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
print("Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
name_index += 1
else: else:
print("Could not retrieve Adobe Adept key.") print("Could not retrieve Adobe Adept key.")
return 0 return 0
@@ -554,12 +580,15 @@ def gui_main():
progpath, progname = os.path.split(argv[0]) progpath, progname = os.path.split(argv[0])
success = False success = False
try: try:
keys = adeptkeys() keys, names = adeptkeys()
print(keys)
print(names)
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
@@ -567,6 +596,7 @@ def gui_main():
keyfileout.write(key) keyfileout.write(key)
success = True success = True
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile)) tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
name_index += 1
except ADEPTError as e: except ADEPTError as e:
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e))) tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception: except Exception:

View File

@@ -45,11 +45,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -93,7 +99,7 @@ def unicode_argv():
return ["kindlekey.py"] return ["kindlekey.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class DrmException(Exception):
pass pass

View File

@@ -41,7 +41,7 @@ def unicode_argv():
return ["DeDRM.py"] return ["DeDRM.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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(): def add_cp65001_codec():

View File

@@ -9,8 +9,9 @@ __license__ = 'GPL v3'
import os, traceback, json, codecs import os, traceback, json, codecs
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
QCheckBox)
from PyQt5 import Qt as QtGui from PyQt5 import Qt as QtGui
from zipfile import ZipFile 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.prefs as prefs
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey 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): class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir): def __init__(self, plugin_path, alfdir):
QWidget.__init__(self) QWidget.__init__(self)
@@ -50,6 +82,9 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials']) self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix'] self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix'] 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 # Start Qt Gui dialog layout
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@@ -100,6 +135,10 @@ class ConfigWidget(QWidget):
self.ereader_button.setToolTip(_("Click to manage keys for eReader ebooks")) self.ereader_button.setToolTip(_("Click to manage keys for eReader ebooks"))
self.ereader_button.setText("eReader ebooks") self.ereader_button.setText("eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys) 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_serial_button)
button_layout.addWidget(self.kindle_android_button) button_layout.addWidget(self.kindle_android_button)
button_layout.addWidget(self.bandn_button) button_layout.addWidget(self.bandn_button)
@@ -107,6 +146,17 @@ class ConfigWidget(QWidget):
button_layout.addWidget(self.ereader_button) button_layout.addWidget(self.ereader_button)
button_layout.addWidget(self.adept_button) button_layout.addWidget(self.adept_button)
button_layout.addWidget(self.kindle_key_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()) self.resize(self.sizeHint())
@@ -148,6 +198,10 @@ class ConfigWidget(QWidget):
d = ManageKeysDialog(self,"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63') d = ManageKeysDialog(self,"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63')
d.exec_() 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 help_link_activated(self, url):
def get_help_file_resource(): def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the # Copy the HTML helpfile to the plugin directory each time the
@@ -170,6 +224,9 @@ class ConfigWidget(QWidget):
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix']) self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix']) self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix'])
self.dedrmprefs.set('configured', True) 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() self.dedrmprefs.writeprefs()
def load_resource(self, name): def load_resource(self, name):
@@ -290,6 +347,8 @@ class ManageKeysDialog(QDialog):
for key in self.plugin_keys: for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key)) self.listy.addItem(QListWidgetItem(key))
self.listy.setMinimumWidth(self.listy.sizeHintForColumn(0) + 20)
def add_key(self): def add_key(self):
d = self.create_key(self) d = self.create_key(self)
d.exec_() d.exec_()
@@ -451,7 +510,6 @@ class ManageKeysDialog(QDialog):
class RenameKeyDialog(QDialog): class RenameKeyDialog(QDialog):
def __init__(self, parent=None,): def __init__(self, parent=None,):
print(repr(self), repr(parent))
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.parent = parent self.parent = parent
self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name)) 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() return str(self.key_ledit.text()).strip()
class AddBandNKeyDialog(QDialog): class AddBandNKeyDialog(QDialog):
def __init__(self, parent=None,): def __init__(self, parent=None,):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
@@ -560,6 +612,11 @@ class AddBandNKeyDialog(QDialog):
data_group_box_layout.addWidget(ccn_disclaimer_label) data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10) 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() key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group) data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Retrieved key:", self)) key_group.addWidget(QLabel("Retrieved key:", self))
@@ -597,13 +654,25 @@ class AddBandNKeyDialog(QDialog):
return str(self.cc_ledit.text()).strip() return str(self.cc_ledit.text()).strip()
def retrieve_key(self): 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 self.chkOldAlgo.isChecked():
if fetched_key == "": # old method, try to generate
errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again." from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) generated_key = generate_bandn_key(self.user_name, self.cc_number)
else: if generated_key == "":
self.key_display.setText(fetched_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): 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(): 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: if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys() defaultkeys, defaultnames = adeptkeys()
else: # linux else: # linux
from .wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(parent.parent.alfdir,"adobekey.py") 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_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: except:
traceback.print_exc()
self.default_key = "" 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: if len(self.default_key)>0:
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
data_group_box = QGroupBox("", self) data_group_box = QGroupBox("", self)
layout.addWidget(data_group_box) layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout() data_group_box_layout = QVBoxLayout()
@@ -732,13 +832,17 @@ class AddAdeptDialog(QDialog):
key_group = QHBoxLayout() key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group) data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Unique Key Name:", self)) key_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("default_key", self) self.key_ledit = QLineEdit(self.default_name_A, self)
self.key_ledit.setToolTip("<p>Enter an identifying name for the current default Adobe Digital Editions key.") 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) key_group.addWidget(self.key_ledit)
self.button_box.accepted.connect(self.accept) self.button_box.accepted.connect(self.accept)
else: else:
default_key_error = QLabel("The default encryption key for Adobe Digital Editions could not be found.", self) # 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) default_key_error.setAlignment(Qt.AlignHCenter)
layout.addWidget(default_key_error) layout.addWidget(default_key_error)
# if no default, bot buttons do the same # if no default, bot buttons do the same
@@ -755,7 +859,7 @@ class AddAdeptDialog(QDialog):
@property @property
def key_value(self): def key_value(self):
return codecs.encode(self.default_key,'hex') return codecs.encode(self.default_key,'hex').decode("utf-8")
def accept(self): def accept(self):
@@ -1013,3 +1117,43 @@ class AddPIDDialog(QDialog):
QDialog.accept(self) 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)

View File

@@ -15,11 +15,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)

View 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

View File

@@ -12,6 +12,7 @@
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf # 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM # 1.01 - Added routine for use by Windows DeDRM
# 2.00 - Python 3, September 2020 # 2.00 - Python 3, September 2020
# 2.01 - Add new Adobe DRM, add Readium LCP
# #
# Written in 2011 by Paul Durrant # Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/ # Released with unlicense. See http://unlicense.org/
@@ -66,11 +67,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -114,7 +121,7 @@ def unicode_argv():
return ["epubtest.py"] return ["epubtest.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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 _FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28 _EXTRA_LEN_OFFSET = 28
@@ -179,7 +186,13 @@ def encryption(infile):
foundencryption = False foundencryption = False
inzip = zipfile.ZipFile(infile,'r') inzip = zipfile.ZipFile(infile,'r')
namelist = set(inzip.namelist()) 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" encryption = "Unencrypted"
else: else:
rights = etree.fromstring(inzip.read('META-INF/rights.xml')) rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
@@ -187,7 +200,9 @@ def encryption(infile):
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172: if len(bookkey) == 172:
encryption = "Adobe" encryption = "Adobe (old)"
if len(bookkey) == 192:
encryption = "Adobe (new)"
elif len(bookkey) == 64: elif len(bookkey) == 64:
encryption = "B&N" encryption = "B&N"
else: else:

View File

@@ -0,0 +1,305 @@
#!/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 = []
count_adept = 0
count_lemonink_invisible = 0
count_lemonink_visible = 0
lemonink_trackingID = None
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
pre_remove = str_new
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)
if (str_new != pre_remove):
count_adept += 1
# Remove eLibri / LemonInk watermark
# Run this in a loop, as it is possible a file has been watermarked twice ...
while True:
pre_remove = str_new
unique_id = re.search(r'<body[^>]+class="[^"]*(t0x[0-9a-fA-F]{25})[^"]*"[^>]*>', str_new)
if (unique_id):
lemonink_trackingID = unique_id.groups()[0]
count_lemonink_invisible += 1
str_new = re.sub(lemonink_trackingID, '', str_new)
pre_remove = str_new
pm = r'(<body[^>]+class="[^"]*"[^>]*>)'
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
str_new = re.sub(pm, r'\1', str_new)
if (str_new != pre_remove):
count_lemonink_visible += 1
else:
break
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 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
if (count_adept > 0):
print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(count_adept))
if (count_lemonink_invisible > 0 or count_lemonink_visible > 0):
print("Watermark: Successfully stripped {0} visible and {1} invisible LemonInk watermark(s) (\"{2}\") from ebook."
.format(count_lemonink_visible, count_lemonink_invisible, lemonink_trackingID))
return output
except:
traceback.print_exc()
return path_to_ebook
# 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
had_amazon = False
had_elibri = False
# 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.
# This regex also matches DuMont watermarks with meta name="watermark", with the case-insensitive match on the "w" in watermark.
pre_remove = container_str_new
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"[Ww]atermark(_\(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=\"[Ww]atermark(_\(hex\))?\"\s*\/>', '', container_str_new)
if pre_remove != container_str_new:
had_amazon = True
# Remove elibri / lemonink watermark
# Lemonink replaces all "id" fields in the opf with "idX_Y", with X being the watermark and Y being a number for that particular ID.
# This regex replaces all "idX_Y" IDs with "id_Y", removing the watermark IDs.
pre_remove = container_str_new
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<\!\-\-\s*Wygenerowane przez elibri dla zamówienia numer [0-9a-fA-F]+\s*\-\-\>', '', container_str_new)
container_str_new = re.sub(r'\=\"id[0-9]+_([0-9]+)\"', r'="id_\1"', container_str_new)
if pre_remove != container_str_new:
had_elibri = True
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
if had_elibri:
print("Watermark: Successfully stripped eLibri watermark from OPF file.")
if had_amazon:
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

View File

@@ -85,10 +85,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -129,7 +136,7 @@ def unicode_argv():
return ["mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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 Des = None
if iswindows: if iswindows:

View File

@@ -559,7 +559,7 @@ class DocParser(object):
if (link > 0): if (link > 0):
linktype = self.link_type[link-1] linktype = self.link_type[link-1]
title = self.link_title[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 = parares[lstart:]
alt_title = alt_title.strip() alt_title = alt_title.strip()
# now strip off the actual printed page number # now strip off the actual printed page number
@@ -770,10 +770,10 @@ class DocParser(object):
first_para_continued = False first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype) (pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not pclass: if not pclass:
if orig_regtype.endswith(b'.right') : pclass = 'cl-right' if orig_regtype.endswith(b'.right') : pclass = b'cl-right'
elif orig_regtype.endswith(b'.center') : pclass = 'cl-center' elif orig_regtype.endswith(b'.center') : pclass = b'cl-center'
elif orig_regtype.endswith(b'.left') : pclass = 'cl-left' elif orig_regtype.endswith(b'.left') : pclass = b'cl-left'
elif orig_regtype.endswith(b'.justify') : pclass = 'cl-justify' elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify'
if pclass and (ptype == 'full') and (len(pclass) >= 6): if pclass and (ptype == 'full') and (len(pclass) >= 6):
tag = 'p' tag = 'p'
if pclass[3:6] == b'h1-' : tag = 'h4' if pclass[3:6] == b'h1-' : tag = 'h4'

View File

@@ -14,11 +14,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)

View File

@@ -52,10 +52,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -97,7 +104,7 @@ def unicode_argv():
return ["ineptepub.py"] return ["ineptepub.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class IGNOBLEError(Exception):

View File

@@ -37,11 +37,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -85,7 +91,7 @@ def unicode_argv():
return ["ignoblekey.py"] return ["ignoblekey.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class DrmException(Exception):
pass pass
@@ -95,7 +101,10 @@ def getNookLogFiles():
logFiles = [] logFiles = []
found = False found = False
if iswindows: 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 # 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 # or the python interface to the 32 vs 64 bit registry is broken

View File

@@ -44,11 +44,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -92,7 +98,7 @@ def unicode_argv():
return ["ignoblekeyfetch.py"] return ["ignoblekeyfetch.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class IGNOBLEError(Exception):

View File

@@ -54,11 +54,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -102,7 +108,7 @@ def unicode_argv():
return ["ignoblekeygen.py"] return ["ignoblekeygen.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class IGNOBLEError(Exception):

View File

@@ -14,6 +14,7 @@
# Revision history: # Revision history:
# 0.1 - Initial alpha testing release 2020 by Pu D. Pud # 0.1 - Initial alpha testing release 2020 by Pu D. Pud
# 0.2 - Python 3 for calibre 5.0 (in testing) # 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' __license__ = 'GPL v3'
__version__ = "0.2" __version__ = "0.3"
import codecs
import sys import sys
import os import os
import re import re
import zlib import zlib
import struct import struct
import hashlib import hashlib
from decimal import * from io import BytesIO
from itertools import chain, islice from decimal import Decimal
import itertools
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
@@ -43,11 +46,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -86,7 +95,7 @@ def unicode_argv():
return ["ignoblepdf.py"] return ["ignoblepdf.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class IGNOBLEError(Exception):
@@ -236,7 +245,6 @@ def _load_crypto():
ARC4, AES = _load_crypto() ARC4, AES = _load_crypto()
from io import BytesIO
# Do we generate cross reference streams on output? # Do we generate cross reference streams on output?
@@ -273,7 +281,7 @@ def nunpack(s, default=0):
elif l == 2: elif l == 2:
return struct.unpack('>H', s)[0] return struct.unpack('>H', s)[0]
elif l == 3: elif l == 3:
return struct.unpack('>L', '\x00'+s)[0] return struct.unpack('>L', bytes([0]) + s)[0]
elif l == 4: elif l == 4:
return struct.unpack('>L', s)[0] return struct.unpack('>L', s)[0]
else: else:
@@ -305,7 +313,7 @@ class PSLiteral(PSObject):
Use PSLiteralTable.intern() instead. Use PSLiteralTable.intern() instead.
''' '''
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name.decode('utf-8')
return return
def __repr__(self): def __repr__(self):
@@ -324,7 +332,7 @@ class PSKeyword(PSObject):
Use PSKeywordTable.intern() instead. Use PSKeywordTable.intern() instead.
''' '''
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name.decode('utf-8')
return return
def __repr__(self): def __repr__(self):
@@ -354,12 +362,12 @@ PSLiteralTable = PSSymbolTable(PSLiteral)
PSKeywordTable = PSSymbolTable(PSKeyword) PSKeywordTable = PSSymbolTable(PSKeyword)
LIT = PSLiteralTable.intern LIT = PSLiteralTable.intern
KWD = PSKeywordTable.intern KWD = PSKeywordTable.intern
KEYWORD_BRACE_BEGIN = KWD('{') KEYWORD_BRACE_BEGIN = KWD(b'{')
KEYWORD_BRACE_END = KWD('}') KEYWORD_BRACE_END = KWD(b'}')
KEYWORD_ARRAY_BEGIN = KWD('[') KEYWORD_ARRAY_BEGIN = KWD(b'[')
KEYWORD_ARRAY_END = KWD(']') KEYWORD_ARRAY_END = KWD(b']')
KEYWORD_DICT_BEGIN = KWD('<<') KEYWORD_DICT_BEGIN = KWD(b'<<')
KEYWORD_DICT_END = KWD('>>') KEYWORD_DICT_END = KWD(b'>>')
def literal_name(x): def literal_name(x):
@@ -381,18 +389,18 @@ def keyword_name(x):
## PSBaseParser ## PSBaseParser
## ##
EOL = re.compile(r'[\r\n]') EOL = re.compile(br'[\r\n]')
SPC = re.compile(r'\s') SPC = re.compile(br'\s')
NONSPC = re.compile(r'\S') NONSPC = re.compile(br'\S')
HEX = re.compile(r'[0-9a-fA-F]') HEX = re.compile(br'[0-9a-fA-F]')
END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') END_LITERAL = re.compile(br'[#/%\[\]()<>{}\s]')
END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') END_HEX_STRING = re.compile(br'[^\s0-9a-fA-F]')
HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') HEX_PAIR = re.compile(br'[0-9a-fA-F]{2}|.')
END_NUMBER = re.compile(r'[^0-9]') END_NUMBER = re.compile(br'[^0-9]')
END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') END_KEYWORD = re.compile(br'[#/%\[\]()<>{}\s]')
END_STRING = re.compile(r'[()\134]') END_STRING = re.compile(br'[()\\]')
OCT_STRING = re.compile(r'[0-7]') OCT_STRING = re.compile(br'[0-7]')
ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
class PSBaseParser(object): class PSBaseParser(object):
@@ -435,7 +443,7 @@ class PSBaseParser(object):
self.fp.seek(pos) self.fp.seek(pos)
# reset the status for nextline() # reset the status for nextline()
self.bufpos = pos self.bufpos = pos
self.buf = '' self.buf = b''
self.charpos = 0 self.charpos = 0
# reset the status for nexttoken() # reset the status for nexttoken()
self.parse1 = self.parse_main self.parse1 = self.parse_main
@@ -457,32 +465,37 @@ class PSBaseParser(object):
if not m: if not m:
return (self.parse_main, len(s)) return (self.parse_main, len(s))
j = m.start(0) 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 self.tokenstart = self.bufpos+j
if c == '%': if c == b'%':
self.token = '%' self.token = c
return (self.parse_comment, j+1) return (self.parse_comment, j+1)
if c == '/': if c == b'/':
self.token = '' self.token = b''
return (self.parse_literal, j+1) return (self.parse_literal, j+1)
if c in '-+' or c.isdigit(): if c in b'-+' or c.isdigit():
self.token = c self.token = c
return (self.parse_number, j+1) return (self.parse_number, j+1)
if c == '.': if c == b'.':
self.token = c self.token = c
return (self.parse_decimal, j+1) return (self.parse_decimal, j+1)
if c.isalpha(): if c.isalpha():
self.token = c self.token = c
return (self.parse_keyword, j+1) return (self.parse_keyword, j+1)
if c == '(': if c == b'(':
self.token = '' self.token = b''
self.paren = 1 self.paren = 1
return (self.parse_string, j+1) return (self.parse_string, j+1)
if c == '<': if c == b'<':
self.token = '' self.token = b''
return (self.parse_wopen, j+1) return (self.parse_wopen, j+1)
if c == '>': if c == b'>':
self.token = '' self.token = b''
return (self.parse_wclose, j+1) return (self.parse_wclose, j+1)
self.add_token(KWD(c)) self.add_token(KWD(c))
return (self.parse_main, j+1) return (self.parse_main, j+1)
@@ -509,20 +522,26 @@ class PSBaseParser(object):
return (self.parse_literal, len(s)) return (self.parse_literal, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = s[j] if isinstance(s[j], str):
if c == '#': c = s[j]
self.hex = '' else:
c = bytes([s[j]])
if c == b'#':
self.hex = b''
return (self.parse_literal_hex, j+1) 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) return (self.parse_main, j)
def parse_literal_hex(self, s, i): 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: if HEX.match(c) and len(self.hex) < 2:
self.hex += c self.hex += c
return (self.parse_literal_hex, i+1) return (self.parse_literal_hex, i+1)
if self.hex: if self.hex:
self.token += chr(int(self.hex, 16)) self.token += bytes([int(self.hex, 16)])
return (self.parse_literal, i) return (self.parse_literal, i)
def parse_number(self, s, i): def parse_number(self, s, i):
@@ -532,8 +551,11 @@ class PSBaseParser(object):
return (self.parse_number, len(s)) return (self.parse_number, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = s[j] if isinstance(s[j], str):
if c == '.': c = s[j]
else:
c = bytes([s[j]])
if c == b'.':
self.token += c self.token += c
return (self.parse_decimal, j+1) return (self.parse_decimal, j+1)
try: try:
@@ -549,7 +571,7 @@ class PSBaseParser(object):
return (self.parse_decimal, len(s)) return (self.parse_decimal, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] 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) return (self.parse_main, j)
def parse_keyword(self, s, i): def parse_keyword(self, s, i):
@@ -575,15 +597,18 @@ class PSBaseParser(object):
return (self.parse_string, len(s)) return (self.parse_string, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = s[j] if isinstance(s[j], str):
if c == '\\': c = s[j]
else:
c = bytes([s[j]])
if c == b'\\':
self.oct = '' self.oct = ''
return (self.parse_string_1, j+1) return (self.parse_string_1, j+1)
if c == '(': if c == b'(':
self.paren += 1 self.paren += 1
self.token += c self.token += c
return (self.parse_string, j+1) return (self.parse_string, j+1)
if c == ')': if c == b')':
self.paren -= 1 self.paren -= 1
if self.paren: if self.paren:
self.token += c self.token += c
@@ -591,42 +616,51 @@ class PSBaseParser(object):
self.add_token(self.token) self.add_token(self.token)
return (self.parse_main, j+1) return (self.parse_main, j+1)
def parse_string_1(self, s, i): 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: if OCT_STRING.match(c) and len(self.oct) < 3:
self.oct += c self.oct += c
return (self.parse_string_1, i+1) return (self.parse_string_1, i+1)
if self.oct: if self.oct:
self.token += chr(int(self.oct, 8)) self.token += bytes([int(self.oct, 8)])
return (self.parse_string, i) return (self.parse_string, i)
if c in ESC_STRING: if c in ESC_STRING:
self.token += chr(ESC_STRING[c]) self.token += bytes([ESC_STRING[c]])
return (self.parse_string, i+1) return (self.parse_string, i+1)
def parse_wopen(self, s, i): def parse_wopen(self, s, i):
c = s[i] if isinstance(s[i], str):
c = s[i]
else:
c = bytes([s[i]])
if c.isspace() or HEX.match(c): if c.isspace() or HEX.match(c):
return (self.parse_hexstring, i) return (self.parse_hexstring, i)
if c == '<': if c == b'<':
self.add_token(KEYWORD_DICT_BEGIN) self.add_token(KEYWORD_DICT_BEGIN)
i += 1 i += 1
return (self.parse_main, i) return (self.parse_main, i)
def parse_wclose(self, s, i): def parse_wclose(self, s, i):
c = s[i] if isinstance(s[i], str):
if c == '>': c = s[i]
else:
c = bytes([s[i]])
if c == b'>':
self.add_token(KEYWORD_DICT_END) self.add_token(KEYWORD_DICT_END)
i += 1 i += 1
return (self.parse_main, i) return (self.parse_main, i)
def parse_hexstring(self, s, i): def parse_hexstring(self, s, i):
m = END_HEX_STRING.search(s, i) m1 = END_HEX_STRING.search(s, i)
if not m: if not m1:
self.token += s[i:] self.token += s[i:]
return (self.parse_hexstring, len(s)) return (self.parse_hexstring, len(s))
j = m.start(0) j = m1.start(0)
self.token += s[i:j] self.token += s[i:j]
token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]),
SPC.sub('', self.token)) SPC.sub(b'', self.token))
self.add_token(token) self.add_token(token)
return (self.parse_main, j) return (self.parse_main, j)
@@ -641,15 +675,15 @@ class PSBaseParser(object):
''' '''
Fetches a next line that ends either with \\r or \\n. Fetches a next line that ends either with \\r or \\n.
''' '''
linebuf = '' linebuf = b''
linepos = self.bufpos + self.charpos linepos = self.bufpos + self.charpos
eol = False eol = False
while 1: while 1:
self.fillbuf() self.fillbuf()
if eol: if eol:
c = self.buf[self.charpos] c = bytes([self.buf[self.charpos]])
# handle '\r\n' # handle '\r\n'
if c == '\n': if c == b'\n':
linebuf += c linebuf += c
self.charpos += 1 self.charpos += 1
break break
@@ -657,7 +691,7 @@ class PSBaseParser(object):
if m: if m:
linebuf += self.buf[self.charpos:m.end(0)] linebuf += self.buf[self.charpos:m.end(0)]
self.charpos = m.end(0) self.charpos = m.end(0)
if linebuf[-1] == '\r': if bytes([linebuf[-1]]) == b'\r':
eol = True eol = True
else: else:
break break
@@ -673,7 +707,7 @@ class PSBaseParser(object):
''' '''
self.fp.seek(0, 2) self.fp.seek(0, 2)
pos = self.fp.tell() pos = self.fp.tell()
buf = '' buf = b''
while 0 < pos: while 0 < pos:
prevpos = pos prevpos = pos
pos = max(0, pos-self.BUFSIZ) pos = max(0, pos-self.BUFSIZ)
@@ -681,13 +715,13 @@ class PSBaseParser(object):
s = self.fp.read(prevpos-pos) s = self.fp.read(prevpos-pos)
if not s: break if not s: break
while 1: while 1:
n = max(s.rfind('\r'), s.rfind('\n')) n = max(s.rfind(b'\r'), s.rfind(b'\n'))
if n == -1: if n == -1:
buf = s + buf buf = s + buf
break break
yield s[n:]+buf yield s[n:]+buf
s = s[:n] s = s[:n]
buf = '' buf = b''
return return
@@ -743,7 +777,7 @@ class PSStackParser(PSBaseParser):
def nextobject(self, direct=False): 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 numbers, arrays and dictionaries. Arrays and dictionaries
are represented as Python sequence and dictionaries. are represented as Python sequence and dictionaries.
''' '''
@@ -753,6 +787,8 @@ class PSStackParser(PSBaseParser):
if (isinstance(token, int) or if (isinstance(token, int) or
isinstance(token, Decimal) or isinstance(token, Decimal) or
isinstance(token, bool) or isinstance(token, bool) or
isinstance(token, bytearray) or
isinstance(token, bytes) or
isinstance(token, str) or isinstance(token, str) or
isinstance(token, PSLiteral)): isinstance(token, PSLiteral)):
# normal token # normal token
@@ -796,10 +832,10 @@ class PSStackParser(PSBaseParser):
return obj return obj
LITERAL_CRYPT = PSLiteralTable.intern('Crypt') LITERAL_CRYPT = PSLiteralTable.intern(b'Crypt')
LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) LITERALS_FLATE_DECODE = (PSLiteralTable.intern(b'FlateDecode'), PSLiteralTable.intern(b'Fl'))
LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) LITERALS_LZW_DECODE = (PSLiteralTable.intern(b'LZWDecode'), PSLiteralTable.intern(b'LZW'))
LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) LITERALS_ASCII85_DECODE = (PSLiteralTable.intern(b'ASCII85Decode'), PSLiteralTable.intern(b'A85'))
## PDF Objects ## PDF Objects
@@ -853,7 +889,7 @@ def resolve_all(x):
if isinstance(x, list): if isinstance(x, list):
x = [ resolve_all(v) for v in x ] x = [ resolve_all(v) for v in x ]
elif isinstance(x, dict): elif isinstance(x, dict):
for (k,v) in x.iteritems(): for (k,v) in iter(x.items()):
x[k] = resolve_all(v) x[k] = resolve_all(v)
return x return x
@@ -861,13 +897,13 @@ def decipher_all(decipher, objid, genno, x):
''' '''
Recursively decipher 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) return decipher(objid, genno, x)
decf = lambda v: decipher_all(decipher, objid, genno, v) decf = lambda v: decipher_all(decipher, objid, genno, v)
if isinstance(x, list): if isinstance(x, list):
x = [decf(v) for v in x] x = [decf(v) for v in x]
elif isinstance(x, dict): 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 return x
@@ -898,7 +934,7 @@ def num_value(x):
def str_value(x): def str_value(x):
x = resolve1(x) x = resolve1(x)
if not isinstance(x, str): if not (isinstance(x, bytearray) or isinstance(x, bytes) or isinstance(x, str)):
if STRICT: if STRICT:
raise PDFTypeError('String required: %r' % x) raise PDFTypeError('String required: %r' % x)
return '' return ''
@@ -931,18 +967,18 @@ def stream_value(x):
# ascii85decode(data) # ascii85decode(data)
def ascii85decode(data): def ascii85decode(data):
n = b = 0 n = b = 0
out = '' out = b''
for c in data: for c in data:
if '!' <= c and c <= 'u': if b'!' <= c and c <= b'u':
n += 1 n += 1
b = b*85+(ord(c)-33) b = b*85+(c-33)
if n == 5: if n == 5:
out += struct.pack('>L',b) out += struct.pack('>L',b)
n = b = 0 n = b = 0
elif c == 'z': elif c == b'z':
assert n == 0 assert n == 0
out += '\0\0\0\0' out += b'\0\0\0\0'
elif c == '~': elif c == b'~':
if n: if n:
for _ in range(5-n): for _ in range(5-n):
b = b*85+84 b = b*85+84
@@ -963,7 +999,7 @@ class PDFStream(PDFObject):
cutdiv = len(rawdata) // 16 cutdiv = len(rawdata) // 16
rawdata = rawdata[:16*cutdiv] rawdata = rawdata[:16*cutdiv]
else: else:
if eol in ('\r', '\n', '\r\n'): if eol in (b'\r', b'\n', b'\r\n'):
rawdata = rawdata[:length] rawdata = rawdata[:length]
self.dic = dic self.dic = dic
@@ -1009,7 +1045,7 @@ class PDFStream(PDFObject):
# will get errors if the document is encrypted. # will get errors if the document is encrypted.
data = zlib.decompress(data) data = zlib.decompress(data)
elif f in LITERALS_LZW_DECODE: 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: elif f in LITERALS_ASCII85_DECODE:
data = ascii85decode(data) data = ascii85decode(data)
elif f == LITERAL_CRYPT: elif f == LITERAL_CRYPT:
@@ -1031,14 +1067,14 @@ class PDFStream(PDFObject):
raise PDFValueError( raise PDFValueError(
'Columns undefined for predictor=12') 'Columns undefined for predictor=12')
columns = int_value(params['Columns']) columns = int_value(params['Columns'])
buf = '' buf = b''
ent0 = '\x00' * columns ent0 = b'\x00' * columns
for i in range(0, len(data), columns+1): for i in range(0, len(data), columns+1):
pred = data[i] pred = data[i]
ent1 = data[i+1:i+1+columns] ent1 = data[i+1:i+1+columns]
if pred == '\x02': if pred == 2:
ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \ ent1 = b''.join(bytes([(a+b) & 255]) \
for (a,b) in zip(ent0,ent1)) for (a,b) in zip(ent0,ent1))
buf += ent1 buf += ent1
ent0 = ent1 ent0 = ent1
data = buf data = buf
@@ -1072,11 +1108,11 @@ class PDFEncryptionError(PDFException): pass
class PDFPasswordIncorrect(PDFEncryptionError): pass class PDFPasswordIncorrect(PDFEncryptionError): pass
# some predefined literals and keywords. # some predefined literals and keywords.
LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm') LITERAL_OBJSTM = PSLiteralTable.intern(b'ObjStm')
LITERAL_XREF = PSLiteralTable.intern('XRef') LITERAL_XREF = PSLiteralTable.intern(b'XRef')
LITERAL_PAGE = PSLiteralTable.intern('Page') LITERAL_PAGE = PSLiteralTable.intern(b'Page')
LITERAL_PAGES = PSLiteralTable.intern('Pages') LITERAL_PAGES = PSLiteralTable.intern(b'Pages')
LITERAL_CATALOG = PSLiteralTable.intern('Catalog') LITERAL_CATALOG = PSLiteralTable.intern(b'Catalog')
## XRefs ## XRefs
@@ -1094,7 +1130,7 @@ class PDFXRef(object):
return '<PDFXRef: objs=%d>' % len(self.offsets) return '<PDFXRef: objs=%d>' % len(self.offsets)
def objids(self): def objids(self):
return self.offsets.iterkeys() return iter(self.offsets.keys())
def load(self, parser): def load(self, parser):
self.offsets = {} self.offsets = {}
@@ -1105,10 +1141,10 @@ class PDFXRef(object):
raise PDFNoValidXRef('Unexpected EOF - file corrupted?') raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
if not line: if not line:
raise PDFNoValidXRef('Premature eof: %r' % parser) raise PDFNoValidXRef('Premature eof: %r' % parser)
if line.startswith('trailer'): if line.startswith(b'trailer'):
parser.seek(pos) parser.seek(pos)
break break
f = line.strip().split(' ') f = line.strip().split(b' ')
if len(f) != 2: if len(f) != 2:
raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line))
try: try:
@@ -1120,16 +1156,17 @@ class PDFXRef(object):
(_, line) = parser.nextline() (_, line) = parser.nextline()
except PSEOF: except PSEOF:
raise PDFNoValidXRef('Unexpected EOF - file corrupted?') raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
f = line.strip().split(' ') f = line.strip().split(b' ')
if len(f) != 3: if len(f) != 3:
raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line))
(pos, genno, use) = f (pos, genno, use) = f
if use != 'n': continue if use != b'n':
self.offsets[objid] = (int(genno), int(pos)) continue
self.offsets[objid] = (int(genno.decode('utf-8')), int(pos.decode('utf-8')))
self.load_trailer(parser) self.load_trailer(parser)
return return
KEYWORD_TRAILER = PSKeywordTable.intern('trailer') KEYWORD_TRAILER = PSKeywordTable.intern(b'trailer')
def load_trailer(self, parser): def load_trailer(self, parser):
try: try:
(_,kwd) = parser.nexttoken() (_,kwd) = parser.nexttoken()
@@ -1180,8 +1217,8 @@ class PDFXRefStream(object):
raise PDFNoValidXRef('Invalid PDF stream spec.') raise PDFNoValidXRef('Invalid PDF stream spec.')
size = stream.dic['Size'] size = stream.dic['Size']
index = stream.dic.get('Index', (0,size)) index = stream.dic.get('Index', (0,size))
self.index = zip(islice(index, 0, None, 2), self.index = list(zip(itertools.islice(index, 0, None, 2),
islice(index, 1, None, 2)) itertools.islice(index, 1, None, 2)))
(self.fl1, self.fl2, self.fl3) = stream.dic['W'] (self.fl1, self.fl2, self.fl3) = stream.dic['W']
self.data = stream.get_data() self.data = stream.get_data()
self.entlen = self.fl1+self.fl2+self.fl3 self.entlen = self.fl1+self.fl2+self.fl3
@@ -1234,7 +1271,8 @@ class PDFDocument(object):
# set_parser(parser) # set_parser(parser)
# Associates the document with an (already initialized) parser object. # Associates the document with an (already initialized) parser object.
def set_parser(self, parser): def set_parser(self, parser):
if self.parser: return if self.parser:
return
self.parser = parser self.parser = parser
# The document is set to be temporarily ready during collecting # The document is set to be temporarily ready during collecting
# all the basic information about the document, e.g. # all the basic information about the document, e.g.
@@ -1256,13 +1294,13 @@ class PDFDocument(object):
dict_value(trailer['Encrypt'])) dict_value(trailer['Encrypt']))
# fix for bad files # fix for bad files
except: except:
self.encryption = ('ffffffffffffffffffffffffffffffffffff', self.encryption = (b'ffffffffffffffffffffffffffffffffffff',
dict_value(trailer['Encrypt'])) dict_value(trailer['Encrypt']))
if 'Root' in trailer: if 'Root' in trailer:
self.set_root(dict_value(trailer['Root'])) self.set_root(dict_value(trailer['Root']))
break break
else: else:
raise PDFSyntaxError('No /Root object! - Is this really a PDF?') raise PDFSyntaxError('No /Root object! - Is this really a PDF?')
# The document is set to be non-ready again, until all the # The document is set to be non-ready again, until all the
# proper initialization (asking the password key and # proper initialization (asking the password key and
# verifying the access permission, so on) is finished. # verifying the access permission, so on) is finished.
@@ -1283,7 +1321,7 @@ class PDFDocument(object):
# Perform the initialization with a given password. # Perform the initialization with a given password.
# This step is mandatory even if there's no password associated # This step is mandatory even if there's no password associated
# with the document. # with the document.
def initialize(self, password=''): def initialize(self, password=b''):
if not self.encryption: if not self.encryption:
self.is_printable = self.is_modifiable = self.is_extractable = True self.is_printable = self.is_modifiable = self.is_extractable = True
self.ready = True self.ready = True
@@ -1310,14 +1348,14 @@ class PDFDocument(object):
def genkey_adobe_ps(self, param): def genkey_adobe_ps(self, param):
# nice little offline principal keys dictionary # nice little offline principal keys dictionary
# global static principal key for German Onleihe / Bibliothek Digital # 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 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') edcdata = str_value(param.get('EDCData')).decode('base64')
pdrllic = str_value(param.get('PDRLLic')).decode('base64') pdrllic = str_value(param.get('PDRLLic')).decode('base64')
pdrlpol = str_value(param.get('PDRLPol')).decode('base64') pdrlpol = str_value(param.get('PDRLPol')).decode('base64')
edclist = [] edclist = []
for pair in edcdata.split('\n'): for pair in edcdata.split(b'\n'):
edclist.append(pair) edclist.append(pair)
# principal key request # principal key request
for key in principalkeys: for key in principalkeys:
@@ -1326,20 +1364,20 @@ class PDFDocument(object):
else: else:
raise IGNOBLEError('Cannot find principal key for this pdf') raise IGNOBLEError('Cannot find principal key for this pdf')
shakey = SHA256(principalkey) shakey = SHA256(principalkey)
ivector = 16 * chr(0) ivector = bytes(16)
plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) 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 ...') raise IGNOBLEError('Offlinekey cannot be decrypted, aborting ...')
pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) 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 ...') raise IGNOBLEError('Could not decrypt PDRLPol, aborting ...')
else: else:
cutter = -1 * ord(pdrlpol[-1]) cutter = -1 * pdrlpol[-1]
pdrlpol = pdrlpol[:cutter] pdrlpol = pdrlpol[:cutter]
return plaintext[:16] return plaintext[:16]
PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
# experimental aes pw support # experimental aes pw support
def initialize_standard(self, password, docid, param): def initialize_standard(self, password, docid, param):
# copy from a global variable # copy from a global variable
@@ -1356,7 +1394,7 @@ class PDFDocument(object):
try: try:
EncMetadata = str_value(param['EncryptMetadata']) EncMetadata = str_value(param['EncryptMetadata'])
except: except:
EncMetadata = 'True' EncMetadata = b'True'
self.is_printable = bool(P & 4) self.is_printable = bool(P & 4)
self.is_modifiable = bool(P & 8) self.is_modifiable = bool(P & 8)
self.is_extractable = bool(P & 16) self.is_extractable = bool(P & 16)
@@ -1373,12 +1411,12 @@ class PDFDocument(object):
hash.update(docid[0]) # 5 hash.update(docid[0]) # 5
# aes special handling if metadata isn't encrypted # aes special handling if metadata isn't encrypted
if EncMetadata == ('False' or 'false'): if EncMetadata == ('False' or 'false'):
hash.update('ffffffff'.decode('hex')) hash.update(codecs.decode(b'ffffffff','hex'))
if 5 <= R: if 5 <= R:
# 8 # 8
for _ in range(50): for _ in range(50):
hash = hashlib.md5(hash.digest()[:length/8]) hash = hashlib.md5(hash.digest()[:length//8])
key = hash.digest()[:length/8] key = hash.digest()[:length//8]
if R == 2: if R == 2:
# Algorithm 3.4 # Algorithm 3.4
u1 = ARC4.new(key).decrypt(password) u1 = ARC4.new(key).decrypt(password)
@@ -1388,7 +1426,7 @@ class PDFDocument(object):
hash.update(docid[0]) # 3 hash.update(docid[0]) # 3
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
for i in range(1,19+1): for i in range(1,19+1):
k = ''.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) x = ARC4.new(k).decrypt(x)
u1 = x+x # 32bytes total u1 = x+x # 32bytes total
if R == 2: if R == 2:
@@ -1410,9 +1448,9 @@ class PDFDocument(object):
if V != 4: if V != 4:
self.decipher = self.decipher_rc4 # XXX may be AES self.decipher = self.decipher_rc4 # XXX may be AES
# aes # aes
elif V == 4 and Length == 128: elif V == 4 and length == 128:
elf.decipher = self.decipher_aes self.decipher = self.decipher_aes
elif V == 4 and Length == 256: elif V == 4 and length == 256:
raise PDFNotImplementedError('AES256 encryption is currently unsupported') raise PDFNotImplementedError('AES256 encryption is currently unsupported')
self.ready = True self.ready = True
return return
@@ -1429,6 +1467,9 @@ class PDFDocument(object):
bookkey = ''.join(rights.findtext(expr)).decode('base64') bookkey = ''.join(rights.findtext(expr)).decode('base64')
bookkey = aes.decrypt(bookkey) bookkey = aes.decrypt(bookkey)
bookkey = bookkey[:-ord(bookkey[-1])] 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:] bookkey = bookkey[-16:]
ebx_V = int_value(param.get('V', 4)) ebx_V = int_value(param.get('V', 4))
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
@@ -1440,18 +1481,18 @@ class PDFDocument(object):
else: else:
V = 2 V = 2
elif len(bookkey) == length + 1: elif len(bookkey) == length + 1:
V = ord(bookkey[0]) V = bookkey[0]
bookkey = bookkey[1:] bookkey = bookkey[1:]
else: else:
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
print("bookkey[0] is %d" % ord(bookkey[0])) print("bookkey[0] is %d" % bookkey[0])
raise IGNOBLEError('error decrypting book session key - mismatched length') raise IGNOBLEError('error decrypting book session key - mismatched length')
else: else:
# proper length unknown try with whatever you have # proper length unknown try with whatever you have
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
print("bookkey[0] is %d" % ord(bookkey[0])) print("bookkey[0] is %d" % bookkey[0])
if ebx_V == 3: if ebx_V == 3:
V = 3 V = 3
else: else:
@@ -1475,7 +1516,7 @@ class PDFDocument(object):
objid = struct.pack('<L', objid ^ 0x3569ac) objid = struct.pack('<L', objid ^ 0x3569ac)
genno = struct.pack('<L', genno ^ 0xca96) genno = struct.pack('<L', genno ^ 0xca96)
key = self.decrypt_key key = self.decrypt_key
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + 'sAlT' key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + b'sAlT'
hash = hashlib.md5(key) hash = hashlib.md5(key)
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)] key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
return key return key
@@ -1484,7 +1525,7 @@ class PDFDocument(object):
def genkey_v4(self, objid, genno): def genkey_v4(self, objid, genno):
objid = struct.pack('<L', objid)[:3] objid = struct.pack('<L', objid)[:3]
genno = struct.pack('<L', genno)[:2] 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) hash = hashlib.md5(key)
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)] key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
return key return key
@@ -1495,8 +1536,7 @@ class PDFDocument(object):
data = data[16:] data = data[16:]
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data) plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
# remove pkcs#5 aes padding # remove pkcs#5 aes padding
cutter = -1 * ord(plaintext[-1]) cutter = -1 * plaintext[-1]
#print(cutter)
plaintext = plaintext[:cutter] plaintext = plaintext[:cutter]
return plaintext return plaintext
@@ -1506,8 +1546,7 @@ class PDFDocument(object):
data = data[16:] data = data[16:]
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data) plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
# remove pkcs#5 aes padding # remove pkcs#5 aes padding
cutter = -1 * ord(plaintext[-1]) cutter = -1 * plaintext[-1]
#print(cutter)
plaintext = plaintext[:cutter] plaintext = plaintext[:cutter]
return plaintext return plaintext
@@ -1516,7 +1555,7 @@ class PDFDocument(object):
return ARC4.new(key).decrypt(data) return ARC4.new(key).decrypt(data)
KEYWORD_OBJ = PSKeywordTable.intern('obj') KEYWORD_OBJ = PSKeywordTable.intern(b'obj')
def getobj(self, objid): def getobj(self, objid):
if not self.ready: if not self.ready:
@@ -1623,11 +1662,11 @@ class PDFParser(PSStackParser):
def __repr__(self): def __repr__(self):
return '<PDFParser>' return '<PDFParser>'
KEYWORD_R = PSKeywordTable.intern('R') KEYWORD_R = PSKeywordTable.intern(b'R')
KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') KEYWORD_ENDOBJ = PSKeywordTable.intern(b'endobj')
KEYWORD_STREAM = PSKeywordTable.intern('stream') KEYWORD_STREAM = PSKeywordTable.intern(b'stream')
KEYWORD_XREF = PSKeywordTable.intern('xref') KEYWORD_XREF = PSKeywordTable.intern(b'xref')
KEYWORD_STARTXREF = PSKeywordTable.intern('startxref') KEYWORD_STARTXREF = PSKeywordTable.intern(b'startxref')
def do_keyword(self, pos, token): def do_keyword(self, pos, token):
if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF):
self.add_results(*self.pop(1)) self.add_results(*self.pop(1))
@@ -1675,8 +1714,8 @@ class PDFParser(PSStackParser):
if STRICT: if STRICT:
raise PDFSyntaxError('Unexpected EOF') raise PDFSyntaxError('Unexpected EOF')
break break
if 'endstream' in line: if b'endstream' in line:
i = line.index('endstream') i = line.index(b'endstream')
objlen += i objlen += i
data += line[:i] data += line[:i]
break break
@@ -1696,7 +1735,7 @@ class PDFParser(PSStackParser):
prev = None prev = None
for line in self.revreadlines(): for line in self.revreadlines():
line = line.strip() line = line.strip()
if line == 'startxref': break if line == b'startxref': break
if line: if line:
prev = line prev = line
else: else:
@@ -1748,7 +1787,7 @@ class PDFParser(PSStackParser):
except PDFNoValidXRef: except PDFNoValidXRef:
# fallback # fallback
self.seek(0) self.seek(0)
pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b') pat = re.compile(b'^(\\d+)\\s+(\\d+)\\s+obj\\b')
offsets = {} offsets = {}
xref = PDFXRef() xref = PDFXRef()
while 1: while 1:
@@ -1756,7 +1795,7 @@ class PDFParser(PSStackParser):
(pos, line) = self.nextline() (pos, line) = self.nextline()
except PSEOF: except PSEOF:
break break
if line.startswith('trailer'): if line.startswith(b'trailer'):
trailerpos = pos # remember last trailer trailerpos = pos # remember last trailer
m = pat.match(line) m = pat.match(line)
if not m: continue if not m: continue
@@ -1783,7 +1822,7 @@ class PDFObjStrmParser(PDFParser):
self.add_results(*self.popall()) self.add_results(*self.popall())
return return
KEYWORD_R = KWD('R') KEYWORD_R = KWD(b'R')
def do_keyword(self, pos, token): def do_keyword(self, pos, token):
if token is self.KEYWORD_R: if token is self.KEYWORD_R:
# reference to indirect object # reference to indirect object
@@ -1826,7 +1865,7 @@ class PDFSerializer(object):
def dump(self, outf): def dump(self, outf):
self.outf = outf self.outf = outf
self.write(self.version) self.write(self.version)
self.write('\n%\xe2\xe3\xcf\xd3\n') self.write(b'\n%\xe2\xe3\xcf\xd3\n')
doc = self.doc doc = self.doc
objids = self.objids objids = self.objids
xrefs = {} xrefs = {}
@@ -1848,18 +1887,18 @@ class PDFSerializer(object):
startxref = self.tell() startxref = self.tell()
if not gen_xref_stm: if not gen_xref_stm:
self.write('xref\n') self.write(b'xref\n')
self.write('0 %d\n' % (maxobj + 1,)) self.write(b'0 %d\n' % (maxobj + 1,))
for objid in range(0, maxobj + 1): for objid in range(0, maxobj + 1):
if objid in xrefs: if objid in xrefs:
# force the genno to be 0 # 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: 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.serialize_object(trailer)
self.write('\nstartxref\n%d\n%%%%EOF' % startxref) self.write(b'\nstartxref\n%d\n%%%%EOF' % startxref)
else: # Generate crossref stream. else: # Generate crossref stream.
@@ -1908,7 +1947,7 @@ class PDFSerializer(object):
data.append(struct.pack('>L', f2)[-fl2:]) data.append(struct.pack('>L', f2)[-fl2:])
data.append(struct.pack('>L', f3)[-fl3:]) data.append(struct.pack('>L', f3)[-fl3:])
index.extend((first, prev - first + 1)) 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, dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index,
'W': [1, fl2, fl3], 'Length': len(data), 'W': [1, fl2, fl3], 'Length': len(data),
'Filter': LITERALS_FLATE_DECODE[0], 'Filter': LITERALS_FLATE_DECODE[0],
@@ -1917,7 +1956,7 @@ class PDFSerializer(object):
dic['Info'] = trailer['Info'] dic['Info'] = trailer['Info']
xrefstm = PDFStream(dic, data) xrefstm = PDFStream(dic, data)
self.serialize_indirect(maxobj, xrefstm) 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): def write(self, data):
self.outf.write(data) self.outf.write(data)
self.last = data[-1:] self.last = data[-1:]
@@ -1926,13 +1965,10 @@ class PDFSerializer(object):
return self.outf.tell() return self.outf.tell()
def escape_string(self, string): def escape_string(self, string):
string = string.replace('\\', '\\\\') string = string.replace(b'\\', b'\\\\')
string = string.replace('\n', r'\n') string = string.replace(b'\n', b'\\n')
string = string.replace('(', r'\(') string = string.replace(b'(', b'\\(')
string = string.replace(')', r'\)') string = string.replace(b')', b'\\)')
# get rid of ciando id
regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}')
if regularexp.match(string): return ('http://www.ciando.com')
return string return string
def serialize_object(self, obj): def serialize_object(self, obj):
@@ -1943,34 +1979,38 @@ class PDFSerializer(object):
obj['Subtype'] = obj['Type'] obj['Subtype'] = obj['Type']
del obj['Type'] del obj['Type']
# end - hope this doesn't have bad effects # end - hope this doesn't have bad effects
self.write('<<') self.write(b'<<')
for key, val in obj.items(): 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.serialize_object(val)
self.write('>>') self.write(b'>>')
elif isinstance(obj, list): elif isinstance(obj, list):
self.write('[') self.write(b'[')
for val in obj: for val in obj:
self.serialize_object(val) 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): 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): elif isinstance(obj, bool):
if self.last.isalnum(): if self.last.isalnum():
self.write(' ') self.write(b' ')
self.write(str(obj).lower()) self.write(str(obj).lower().encode('utf-8'))
elif isinstance(obj, (int, long)): elif isinstance(obj, (int, long)):
if self.last.isalnum(): if self.last.isalnum():
self.write(' ') self.write(b' ')
self.write(str(obj)) self.write(str(obj).encode('utf-8'))
elif isinstance(obj, Decimal): elif isinstance(obj, Decimal):
if self.last.isalnum(): if self.last.isalnum():
self.write(' ') self.write(b' ')
self.write(str(obj)) self.write(str(obj).encode('utf-8'))
elif isinstance(obj, PDFObjRef): elif isinstance(obj, PDFObjRef):
if self.last.isalnum(): if self.last.isalnum():
self.write(' ') self.write(b' ')
self.write('%d %d R' % (obj.objid, 0)) self.write(b'%d %d R' % (obj.objid, 0))
elif isinstance(obj, PDFStream): elif isinstance(obj, PDFStream):
### If we don't generate cross ref streams the object streams ### If we don't generate cross ref streams the object streams
### are no longer useful, as we have extracted all objects from ### are no longer useful, as we have extracted all objects from
@@ -1980,21 +2020,21 @@ class PDFSerializer(object):
else: else:
data = obj.get_decdata() data = obj.get_decdata()
self.serialize_object(obj.dic) self.serialize_object(obj.dic)
self.write('stream\n') self.write(b'stream\n')
self.write(data) self.write(data)
self.write('\nendstream') self.write(b'\nendstream')
else: else:
data = str(obj) data = str(obj).encode('utf-8')
if data[0].isalnum() and self.last.isalnum(): if bytes([data[0]]).isalnum() and self.last.isalnum():
self.write(' ') self.write(b' ')
self.write(data) self.write(data)
def serialize_indirect(self, objid, obj): def serialize_indirect(self, objid, obj):
self.write('%d 0 obj' % (objid,)) self.write(b'%d 0 obj' % (objid,))
self.serialize_object(obj) self.serialize_object(obj)
if self.last.isalnum(): if self.last.isalnum():
self.write('\n') self.write(b'\n')
self.write('endobj\n') self.write(b'endobj\n')
@@ -2003,12 +2043,7 @@ def decryptBook(userkey, inpath, outpath):
if AES is None: if AES is None:
raise IGNOBLEError("PyCrypto or OpenSSL must be installed.") raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
with open(inpath, 'rb') as inf: with open(inpath, 'rb') as inf:
#try:
serializer = PDFSerializer(inf, userkey) 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: with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end # help construct to make sure the method runs to the end
try: try:

View File

@@ -46,7 +46,7 @@ import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing from contextlib import closing
import xml.etree.ElementTree as etree from lxml import etree
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get # and also make sure that any unicode strings get
@@ -58,11 +58,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -104,12 +110,15 @@ def unicode_argv():
return ["ineptepub.py"] return ["ineptepub.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class ADEPTError(Exception):
pass pass
class ADEPTNewVersionError(Exception):
pass
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast Structure, c_ulong, create_string_buffer, cast
@@ -333,7 +342,7 @@ def _load_crypto():
AES, RSA = _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', NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'} 'enc': 'http://www.w3.org/2001/04/xmlenc#'}
@@ -343,13 +352,35 @@ class Decryptor(object):
self._aes = AES(bookkey) self._aes = AES(bookkey)
encryption = etree.fromstring(encryption) encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set() 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'), expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference')) enc('CipherReference'))
for elem in encryption.findall(expr): for elem in encryption.findall(expr):
path = elem.get('URI', None) path = elem.get('URI', None)
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
if path is not None: if path is not None:
path = path.encode('utf-8') if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"):
encrypted.add(path) # 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): def decompress(self, bytes):
dc = zlib.decompressobj(-15) dc = zlib.decompressobj(-15)
@@ -393,12 +424,48 @@ def adeptBook(inpath):
return True return True
return False 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): def decryptBook(userkey, inpath, outpath):
if AES is None: if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed.") raise ADEPTError("PyCrypto or OpenSSL must be installed.")
rsa = RSA(userkey) rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf: with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist()) namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \ if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist: 'META-INF/encryption.xml' not in namelist:
print("{0:s} is DRM-free.".format(os.path.basename(inpath))) print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
@@ -410,41 +477,46 @@ def decryptBook(userkey, inpath, outpath):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 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: if len(bookkey) != 172:
print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))) print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
return 1 return 1
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64')) bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
# Padded as per RSAES-PKCS1-v1_5 # Padded as per RSAES-PKCS1-v1_5
if len(bookkey) != 16: if len(bookkey) > 16:
if bookkey[-17] != '\x00' and bookkey[-17] != 0: if verify_book_key(bookkey):
bookkey = bookkey[-16:]
else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2 return 2
else:
bookkey = bookkey[-16:]
encryption = inf.read('META-INF/encryption.xml') encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption) decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype')
zi.compress_type=ZIP_STORED for path in (["mimetype"] + namelist):
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path) data = inf.read(path)
zi = ZipInfo(path) zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED 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: try:
# get the file info, including time-stamp # get the file info, including time-stamp
oldzi = inf.getinfo(path) oldzi = inf.getinfo(path)
@@ -456,9 +528,15 @@ def decryptBook(userkey, inpath, outpath):
# external attributes are dependent on the create system, so copy both. # external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system 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: except:
pass 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: except:
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
return 2 return 2

View File

@@ -76,11 +76,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -119,12 +125,15 @@ def unicode_argv():
return ["ineptpdf.py"] return ["ineptpdf.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class ADEPTError(Exception):
pass pass
class ADEPTNewVersionError(Exception):
pass
import hashlib import hashlib
@@ -442,7 +451,7 @@ def nunpack(s, default=0):
elif l == 2: elif l == 2:
return struct.unpack('>H', s)[0] return struct.unpack('>H', s)[0]
elif l == 3: elif l == 3:
return struct.unpack('>L', '\x00'+s)[0] return struct.unpack('>L', bytes([0]) + s)[0]
elif l == 4: elif l == 4:
return struct.unpack('>L', s)[0] return struct.unpack('>L', s)[0]
else: else:
@@ -550,17 +559,17 @@ def keyword_name(x):
## PSBaseParser ## PSBaseParser
## ##
EOL = re.compile(rb'[\r\n]') EOL = re.compile(br'[\r\n]')
SPC = re.compile(rb'\s') SPC = re.compile(br'\s')
NONSPC = re.compile(rb'\S') NONSPC = re.compile(br'\S')
HEX = re.compile(rb'[0-9a-fA-F]') HEX = re.compile(br'[0-9a-fA-F]')
END_LITERAL = re.compile(rb'[#/%\[\]()<>{}\s]') END_LITERAL = re.compile(br'[#/%\[\]()<>{}\s]')
END_HEX_STRING = re.compile(rb'[^\s0-9a-fA-F]') END_HEX_STRING = re.compile(br'[^\s0-9a-fA-F]')
HEX_PAIR = re.compile(rb'[0-9a-fA-F]{2}|.') HEX_PAIR = re.compile(br'[0-9a-fA-F]{2}|.')
END_NUMBER = re.compile(rb'[^0-9]') END_NUMBER = re.compile(br'[^0-9]')
END_KEYWORD = re.compile(rb'[#/%\[\]()<>{}\s]') END_KEYWORD = re.compile(br'[#/%\[\]()<>{}\s]')
END_STRING = re.compile(rb'[()\\]') END_STRING = re.compile(br'[()\\]')
OCT_STRING = re.compile(rb'[0-7]') OCT_STRING = re.compile(br'[0-7]')
ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 } ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
class PSBaseParser(object): class PSBaseParser(object):
@@ -625,7 +634,12 @@ class PSBaseParser(object):
if not m: if not m:
return (self.parse_main, len(s)) return (self.parse_main, len(s))
j = m.start(0) j = m.start(0)
c = bytes([s[j]]) if isinstance(s[j], str):
# Python 2
c = s[j]
else:
# Python 3
c = bytes([s[j]])
self.tokenstart = self.bufpos+j self.tokenstart = self.bufpos+j
if c == b'%': if c == b'%':
self.token = c self.token = c
@@ -677,7 +691,10 @@ class PSBaseParser(object):
return (self.parse_literal, len(s)) return (self.parse_literal, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = bytes([s[j]]) if isinstance(s[j], str):
c = s[j]
else:
c = bytes([s[j]])
if c == b'#': if c == b'#':
self.hex = b'' self.hex = b''
return (self.parse_literal_hex, j+1) return (self.parse_literal_hex, j+1)
@@ -685,7 +702,10 @@ class PSBaseParser(object):
return (self.parse_main, j) return (self.parse_main, j)
def parse_literal_hex(self, s, i): def parse_literal_hex(self, s, i):
c = bytes([s[i]]) if isinstance(s[i], str):
c = s[i]
else:
c = bytes([s[i]])
if HEX.match(c) and len(self.hex) < 2: if HEX.match(c) and len(self.hex) < 2:
self.hex += c self.hex += c
return (self.parse_literal_hex, i+1) return (self.parse_literal_hex, i+1)
@@ -700,7 +720,10 @@ class PSBaseParser(object):
return (self.parse_number, len(s)) return (self.parse_number, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = bytes([s[j]]) if isinstance(s[j], str):
c = s[j]
else:
c = bytes([s[j]])
if c == b'.': if c == b'.':
self.token += c self.token += c
return (self.parse_decimal, j+1) return (self.parse_decimal, j+1)
@@ -743,7 +766,10 @@ class PSBaseParser(object):
return (self.parse_string, len(s)) return (self.parse_string, len(s))
j = m.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
c = bytes([s[j]]) if isinstance(s[j], str):
c = s[j]
else:
c = bytes([s[j]])
if c == b'\\': if c == b'\\':
self.oct = '' self.oct = ''
return (self.parse_string_1, j+1) return (self.parse_string_1, j+1)
@@ -760,7 +786,10 @@ class PSBaseParser(object):
return (self.parse_main, j+1) return (self.parse_main, j+1)
def parse_string_1(self, s, i): def parse_string_1(self, s, i):
c = bytes([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: if OCT_STRING.match(c) and len(self.oct) < 3:
self.oct += c self.oct += c
return (self.parse_string_1, i+1) return (self.parse_string_1, i+1)
@@ -772,7 +801,10 @@ class PSBaseParser(object):
return (self.parse_string, i+1) return (self.parse_string, i+1)
def parse_wopen(self, s, i): def parse_wopen(self, s, i):
c = bytes([s[i]]) if isinstance(s[i], str):
c = s[i]
else:
c = bytes([s[i]])
if c.isspace() or HEX.match(c): if c.isspace() or HEX.match(c):
return (self.parse_hexstring, i) return (self.parse_hexstring, i)
if c == b'<': if c == b'<':
@@ -781,7 +813,10 @@ class PSBaseParser(object):
return (self.parse_main, i) return (self.parse_main, i)
def parse_wclose(self, s, i): def parse_wclose(self, s, i):
c = bytes([s[i]]) if isinstance(s[i], str):
c = s[i]
else:
c = bytes([s[i]])
if c == b'>': if c == b'>':
self.add_token(KEYWORD_DICT_END) self.add_token(KEYWORD_DICT_END)
i += 1 i += 1
@@ -923,6 +958,7 @@ class PSStackParser(PSBaseParser):
isinstance(token, bool) or isinstance(token, bool) or
isinstance(token, bytearray) or isinstance(token, bytearray) or
isinstance(token, bytes) or isinstance(token, bytes) or
isinstance(token, str) or
isinstance(token, PSLiteral)): isinstance(token, PSLiteral)):
# normal token # normal token
self.push((pos, token)) self.push((pos, token))
@@ -1030,7 +1066,7 @@ def decipher_all(decipher, objid, genno, x):
''' '''
Recursively decipher X. Recursively decipher X.
''' '''
if isinstance(x, bytearray) or isinstance(x,bytes): if isinstance(x, bytearray) or isinstance(x,bytes) or isinstance(x,str):
return decipher(objid, genno, x) return decipher(objid, genno, x)
decf = lambda v: decipher_all(decipher, objid, genno, v) decf = lambda v: decipher_all(decipher, objid, genno, v)
if isinstance(x, list): if isinstance(x, list):
@@ -1067,7 +1103,7 @@ def num_value(x):
def str_value(x): def str_value(x):
x = resolve1(x) x = resolve1(x)
if not (isinstance(x, bytearray) or isinstance(x, bytes)): if not (isinstance(x, bytearray) or isinstance(x, bytes) or isinstance(x, str)):
if STRICT: if STRICT:
raise PDFTypeError('String required: %r' % x) raise PDFTypeError('String required: %r' % x)
return '' return ''
@@ -1204,7 +1240,7 @@ class PDFStream(PDFObject):
for i in range(0, len(data), columns+1): for i in range(0, len(data), columns+1):
pred = data[i] pred = data[i]
ent1 = data[i+1:i+1+columns] ent1 = data[i+1:i+1+columns]
if pred == b'\x02': if pred == 2:
ent1 = b''.join(bytes([(a+b) & 255]) \ ent1 = b''.join(bytes([(a+b) & 255]) \
for (a,b) in zip(ent0,ent1)) for (a,b) in zip(ent0,ent1))
buf += ent1 buf += ent1
@@ -1349,8 +1385,8 @@ class PDFXRefStream(object):
raise PDFNoValidXRef('Invalid PDF stream spec.') raise PDFNoValidXRef('Invalid PDF stream spec.')
size = stream.dic['Size'] size = stream.dic['Size']
index = stream.dic.get('Index', (0,size)) index = stream.dic.get('Index', (0,size))
self.index = zip(itertools.islice(index, 0, None, 2), self.index = list(zip(itertools.islice(index, 0, None, 2),
itertools.islice(index, 1, None, 2)) itertools.islice(index, 1, None, 2)))
(self.fl1, self.fl2, self.fl3) = stream.dic['W'] (self.fl1, self.fl2, self.fl3) = stream.dic['W']
self.data = stream.get_data() self.data = stream.get_data()
self.entlen = self.fl1+self.fl2+self.fl3 self.entlen = self.fl1+self.fl2+self.fl3
@@ -1417,7 +1453,6 @@ class PDFDocument(object):
for xref in self.xrefs: for xref in self.xrefs:
trailer = xref.trailer trailer = xref.trailer
if not trailer: continue if not trailer: continue
# If there's an encryption info, remember it. # If there's an encryption info, remember it.
if 'Encrypt' in trailer: if 'Encrypt' in trailer:
#assert not self.encryption #assert not self.encryption
@@ -1453,7 +1488,7 @@ class PDFDocument(object):
# Perform the initialization with a given password. # Perform the initialization with a given password.
# This step is mandatory even if there's no password associated # This step is mandatory even if there's no password associated
# with the document. # with the document.
def initialize(self, password=b''): def initialize(self, password=b'', inept=True):
if not self.encryption: if not self.encryption:
self.is_printable = self.is_modifiable = self.is_extractable = True self.is_printable = self.is_modifiable = self.is_extractable = True
self.ready = True self.ready = True
@@ -1465,8 +1500,11 @@ class PDFDocument(object):
return self.initialize_adobe_ps(password, docid, param) return self.initialize_adobe_ps(password, docid, param)
if type == 'Standard': if type == 'Standard':
return self.initialize_standard(password, docid, param) return self.initialize_standard(password, docid, param)
if type == 'EBX_HANDLER': if type == 'EBX_HANDLER' and inept is True:
return self.initialize_ebx(password, docid, param) return self.initialize_ebx_inept(password, docid, param)
if type == 'EBX_HANDLER' and inept is False:
return self.initialize_ebx_ignoble(password, docid, param)
raise PDFEncryptionError('Unknown filter: param=%r' % param) raise PDFEncryptionError('Unknown filter: param=%r' % param)
def initialize_adobe_ps(self, password, docid, param): def initialize_adobe_ps(self, password, docid, param):
@@ -1482,7 +1520,7 @@ class PDFDocument(object):
# global static principal key for German Onleihe / Bibliothek Digital # global static principal key for German Onleihe / Bibliothek Digital
principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')} principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')}
self.is_printable = self.is_modifiable = self.is_extractable = True 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') edcdata = str_value(param.get('EDCData')).decode('base64')
pdrllic = str_value(param.get('PDRLLic')).decode('base64') pdrllic = str_value(param.get('PDRLLic')).decode('base64')
pdrlpol = str_value(param.get('PDRLPol')).decode('base64') pdrlpol = str_value(param.get('PDRLPol')).decode('base64')
@@ -1547,8 +1585,8 @@ class PDFDocument(object):
if 5 <= R: if 5 <= R:
# 8 # 8
for _ in range(50): for _ in range(50):
hash = hashlib.md5(hash.digest()[:length/8]) hash = hashlib.md5(hash.digest()[:length//8])
key = hash.digest()[:length/8] key = hash.digest()[:length//8]
if R == 2: if R == 2:
# Algorithm 3.4 # Algorithm 3.4
u1 = ARC4.new(key).decrypt(password) u1 = ARC4.new(key).decrypt(password)
@@ -1587,23 +1625,98 @@ class PDFDocument(object):
self.ready = True self.ready = True
return return
def initialize_ebx(self, password, docid, param): def verify_book_key(self, bookkey):
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
# Byte not null, invalid result
return False
if ((bookkey[0] != '\x02' and bookkey[0] != 2) and
((bookkey[0] != '\x00' and bookkey[0] != 0) or
(bookkey[1] != '\x02' and bookkey[1] != 2))):
# Key not starting with "00 02" or "02" -> error
return False
keylen = len(bookkey) - 17
for i in range(1, keylen):
if bookkey[i] == 0 or bookkey[i] == '\x00':
# Padding data contains a space - that's not allowed.
# Probably bad decryption.
return False
return True
def initialize_ebx_ignoble(self, keyb64, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True
key = keyb64.decode('base64')[:16]
aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key))
length = int_value(param.get('Length', 0)) / 8
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}encryptedKey'
bookkey = ''.join(rights.findtext(expr)).decode('base64')
bookkey = aes.decrypt(bookkey)
bookkey = bookkey[:-ord(bookkey[-1])]
bookkey = bookkey[-16:]
ebx_V = int_value(param.get('V', 4))
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
# added because of improper booktype / decryption book session key errors
if length > 0:
if len(bookkey) == length:
if ebx_V == 3:
V = 3
else:
V = 2
elif len(bookkey) == length + 1:
V = bookkey[0]
bookkey = bookkey[1:]
else:
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
print("bookkey[0] is %d" % bookkey[0])
raise ADEPTError('error decrypting book session key - mismatched length')
else:
# proper length unknown try with whatever you have
print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
print("bookkey[0] is %d" % ord(bookkey[0]))
if ebx_V == 3:
V = 3
else:
V = 2
self.decrypt_key = bookkey
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
self.decipher = self.decrypt_rc4
self.ready = True
return
def initialize_ebx_inept(self, password, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True self.is_printable = self.is_modifiable = self.is_extractable = True
rsa = RSA(password) rsa = RSA(password)
length = int_value(param.get('Length', 0)) / 8 length = int_value(param.get('Length', 0)) // 8
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64') rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
rights = zlib.decompress(rights, -15) rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights) rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}encryptedKey' expr = './/{http://ns.adobe.com/adept}encryptedKey'
bookkey = codecs.decode(''.join(rights.findtext(expr)).encode('utf-8'),'base64') bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 192:
print("This seems to be an Adobe ADEPT PDF with Adobe's new DRM")
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")
bookkey = codecs.decode(bookkey.encode('utf-8'),'base64')
bookkey = rsa.decrypt(bookkey) bookkey = rsa.decrypt(bookkey)
#if bookkey[0] != 2:
# raise ADEPTError('error decrypting book session key') if len(bookkey) > 16:
try: if (self.verify_book_key(bookkey)):
index = bookkey.index(b'\0') + 1 bookkey = bookkey[-16:]
bookkey = bookkey[index:] length = 16
except ValueError: else:
pass raise ADEPTError('error decrypting book session key')
ebx_V = int_value(param.get('V', 4)) ebx_V = int_value(param.get('V', 4))
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
# added because of improper booktype / decryption book session key errors # added because of improper booktype / decryption book session key errors
@@ -1920,7 +2033,7 @@ class PDFParser(PSStackParser):
except PDFNoValidXRef: except PDFNoValidXRef:
# fallback # fallback
self.seek(0) self.seek(0)
pat = re.compile(rb'^(\d+)\s+(\d+)\s+obj\b') pat = re.compile(b'^(\\d+)\\s+(\\d+)\\s+obj\\b')
offsets = {} offsets = {}
xref = PDFXRef() xref = PDFXRef()
while 1: while 1:
@@ -1971,18 +2084,48 @@ class PDFObjStrmParser(PDFParser):
self.push((pos, token)) self.push((pos, token))
return return
# Takes a PDF file name as input, and if this is an ADE-protected PDF,
# returns the UUID of the user that's licensed to open this file.
def adeptGetUserUUID(inf):
try:
doc = PDFDocument()
inf = open(inf, 'rb')
pars = PDFParser(doc, inf)
(docid, param) = doc.encryption
type = literal_name(param['Filter'])
if type != 'EBX_HANDLER':
# No EBX_HANDLER, no idea which user key can decrypt this.
inf.close()
return None
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
inf.close()
rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}user'
user_uuid = ''.join(rights.findtext(expr))
if user_uuid[:9] != "urn:uuid:":
return None
return user_uuid[9:]
except:
return None
### ###
### My own code, for which there is none else to blame ### My own code, for which there is none else to blame
class PDFSerializer(object): class PDFSerializer(object):
def __init__(self, inf, userkey): def __init__(self, inf, userkey, inept=True):
global GEN_XREF_STM, gen_xref_stm global GEN_XREF_STM, gen_xref_stm
gen_xref_stm = GEN_XREF_STM > 1 gen_xref_stm = GEN_XREF_STM > 1
self.version = inf.read(8) self.version = inf.read(8)
inf.seek(0) inf.seek(0)
self.doc = doc = PDFDocument() self.doc = doc = PDFDocument()
parser = PDFParser(doc, inf) parser = PDFParser(doc, inf)
doc.initialize(userkey) doc.initialize(userkey, inept)
self.objids = objids = set() self.objids = objids = set()
for xref in reversed(doc.xrefs): for xref in reversed(doc.xrefs):
trailer = xref.trailer trailer = xref.trailer
@@ -2099,9 +2242,9 @@ class PDFSerializer(object):
def escape_string(self, string): def escape_string(self, string):
string = string.replace(b'\\', b'\\\\') string = string.replace(b'\\', b'\\\\')
string = string.replace(b'\n', rb'\n') string = string.replace(b'\n', b'\\n')
string = string.replace(b'(', rb'\(') string = string.replace(b'(', b'\\(')
string = string.replace(b')', rb'\)') string = string.replace(b')', b'\\)')
return string return string
def serialize_object(self, obj): def serialize_object(self, obj):
@@ -2172,11 +2315,11 @@ class PDFSerializer(object):
def decryptBook(userkey, inpath, outpath): def decryptBook(userkey, inpath, outpath, inept=True):
if RSA is None: if RSA is None:
raise ADEPTError("PyCryptodome or OpenSSL must be installed.") raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
with open(inpath, 'rb') as inf: with open(inpath, 'rb') as inf:
serializer = PDFSerializer(inf, userkey) serializer = PDFSerializer(inf, userkey, inept)
with open(outpath, 'wb') as outf: with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end # help construct to make sure the method runs to the end
try: try:

View File

@@ -512,6 +512,8 @@ class BinaryIonParser(object):
if table is not None: if table is not None:
self.symbols.import_(table, min(maxid, len(table.symnames))) self.symbols.import_(table, min(maxid, len(table.symnames)))
if len(table.symnames) < maxid:
self.symbols.importunknown(name + "-unknown", maxid - len(table.symnames))
else: else:
self.symbols.importunknown(name, maxid) self.symbols.importunknown(name, maxid)
@@ -733,7 +735,10 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
'com.amazon.drm.EncryptedPage@2.0', 'com.amazon.drm.EncryptedPage@2.0',
'com.amazon.drm.PlainText@2.0', 'compression_algorithm', 'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
'com.amazon.drm.Compressed@1.0', 'page_index_table', '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): def addprottable(ion):
ion.addtocatalog("ProtectedData", 1, SYM_NAMES) ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
@@ -757,9 +762,45 @@ def pkcs7unpad(msg, blocklen):
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret # every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
VOUCHER_VERSION_INFOS = { OBFUSCATION_TABLE = {
2: [b'Antidisestablishmentarianism', 5], "V1": (0x00, None),
3: [b'Floccinaucinihilipilification', 8] "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'),
} }
@@ -768,9 +809,7 @@ def obfuscate(secret, version):
if version == 1: # v1 does not use obfuscation if version == 1: # v1 does not use obfuscation
return secret return secret
params = VOUCHER_VERSION_INFOS[version] magic, word = OBFUSCATION_TABLE["V%d" % version]
word = params[0]
magic = params[1]
# extend secret so that its length is divisible by the magic number # extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0: if len(secret) % magic != 0:
@@ -809,24 +848,30 @@ class DrmIonVoucher(object):
def __init__(self, voucherenv, dsn, secret): 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 = [] self.lockparams = []
self.envelope = BinaryIonParser(voucherenv) self.envelope = BinaryIonParser(voucherenv)
addprottable(self.envelope) addprottable(self.envelope)
def decryptvoucher(self): def decryptvoucher(self):
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII')
self.lockparams.sort() self.lockparams.sort()
for param in self.lockparams: for param in self.lockparams:
if param == "ACCOUNT_SECRET": if param == "ACCOUNT_SECRET":
shared += param + self.secret shared += param.encode('ASCII') + self.secret
elif param == "CLIENT_ID": elif param == "CLIENT_ID":
shared += param + self.dsn shared += param.encode('ASCII') + self.dsn
else: else:
_assert(False, "Unknown lock parameter: %s" % param) _assert(False, "Unknown lock parameter: %s" % param)
sharedsecret = obfuscate(shared.encode('ASCII'), self.version) sharedsecret = obfuscate(shared, self.version)
key = hmac.new(sharedsecret, b"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]) aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
@@ -993,6 +1038,7 @@ class DrmIon(object):
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]: elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
decompress = False decompress = False
decrypt = True
ct = None ct = None
civ = None civ = None
self.ion.stepin() self.ion.stepin()
@@ -1006,7 +1052,23 @@ class DrmIon(object):
civ = self.ion.lobvalue() civ = self.ion.lobvalue()
if ct is not None and civ is not None: 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()
self.ion.stepout() self.ion.stepout()
@@ -1017,9 +1079,12 @@ class DrmIon(object):
def print_(self, lst): def print_(self, lst):
self.ion.print_(lst) self.ion.print_(lst)
def processpage(self, ct, civ, outpages, decompress): def processpage(self, ct, civ, outpages, decompress, decrypt):
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) if decrypt:
msg = pkcs7unpad(aes.decrypt(ct), 16) aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
else:
msg = ct
if not decompress: if not decompress:
outpages.write(msg) outpages.write(msg)

View File

@@ -103,11 +103,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -148,7 +154,7 @@ def unicode_argv():
return ["mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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 # cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py # borrowed from calibre from calibre/src/calibre/__init__.py
@@ -191,7 +197,7 @@ def unescape(text):
else: else:
# named entity # named entity
try: try:
text = chr(htmlentitydefs.name2codepoint[text[1:-1]]) text = chr(html.entities.name2codepoint[text[1:-1]])
except KeyError: except KeyError:
pass pass
return text # leave as is return text # leave as is

View File

@@ -4,10 +4,13 @@
# Engine to remove drm from Kindle KFX ebooks # Engine to remove drm from Kindle KFX ebooks
# 2.0 - Python 3 for calibre 5.0 # 2.0 - Python 3 for calibre 5.0
# 2.1 - Some fixes for debugging
# 2.1.1 - Whitespace!
import os import os
import shutil import shutil
import traceback
import zipfile import zipfile
from io import BytesIO from io import BytesIO
@@ -65,6 +68,9 @@ class KFXZipBook:
print("Decrypting KFX DRM voucher: {0}".format(info.filename)) print("Decrypting KFX DRM voucher: {0}".format(info.filename))
for pid in [''] + totalpids: 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)]: 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: if len(pid) == dsn_len + secret_len:
break # split pid into DSN and account secret break # split pid into DSN and account secret
@@ -77,6 +83,7 @@ class KFXZipBook:
voucher.decryptvoucher() voucher.decryptvoucher()
break break
except: except:
traceback.print_exc()
pass pass
else: else:
raise Exception("Failed to decrypt KFX DRM voucher with any key") raise Exception("Failed to decrypt KFX DRM voucher with any key")
@@ -85,8 +92,10 @@ class KFXZipBook:
license_type = voucher.getlicensetype() license_type = voucher.getlicensetype()
if license_type != "Purchase": if license_type != "Purchase":
raise Exception(("This book is licensed as {0}. " #raise Exception(("This book is licensed as {0}. "
'These tools are intended for use on purchased books.').format(license_type)) # '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 self.voucher = voucher

View File

@@ -174,14 +174,14 @@ def pidFromSerial(s, l):
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. # Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
def getKindlePids(rec209, token, serialnum): def getKindlePids(rec209, token, serialnum):
if isinstance(serialnum,str):
serialnum = serialnum.encode('utf-8')
if rec209 is None: if rec209 is None:
return [serialnum] return [serialnum]
pids=[] pids=[]
if isinstance(serialnum,str):
serialnum = serialnum.encode('utf-8')
# Compute book PID # Compute book PID
pidHash = SHA1(serialnum+rec209+token) pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash) bookPID = encodePID(pidHash)
@@ -209,7 +209,7 @@ def getK4Pids(rec209, token, kindleDatabase):
kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens']) kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens'])
except KeyError: except KeyError:
kindleAccountToken="" kindleAccountToken = b''
pass pass
try: try:

View File

@@ -61,11 +61,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -109,7 +115,7 @@ def unicode_argv():
return ["kindlekey.py"] return ["kindlekey.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class DrmException(Exception):
pass pass
@@ -189,7 +195,11 @@ if iswindows:
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast string_at, Structure, c_void_p, cast
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
MAX_PATH = 255 MAX_PATH = 255
kernel32 = windll.kernel32 kernel32 = windll.kernel32
advapi32 = windll.advapi32 advapi32 = windll.advapi32

View File

@@ -26,11 +26,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -71,7 +77,7 @@ def unicode_argv():
return ["kindlepid.py"] return ["kindlepid.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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' letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'

View File

@@ -94,11 +94,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -139,7 +145,7 @@ def unicode_argv():
return ["mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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): class DrmException(Exception):
@@ -320,6 +326,15 @@ class MobiBook:
elif type == 404 and size == 9: elif type == 404 and size == 9:
# make sure text to speech is enabled # make sure text to speech is enabled
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8) 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') # print type, size, content, content.encode('hex')
pos += size pos += size
except Exception as e: except Exception as e:
@@ -446,7 +461,8 @@ class MobiBook:
data406 = self.meta_array[406] data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406) val406, = struct.unpack('>Q',data406)
if val406 != 0: 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 = [] goodpids = []
# print("DEBUG ==== pidlist = ", pidlist) # print("DEBUG ==== pidlist = ", pidlist)

View File

@@ -19,6 +19,8 @@ class DeDRM_Prefs():
self.dedrmprefs = JSONConfig(JSON_PATH) self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False self.dedrmprefs.defaults['configured'] = False
self.dedrmprefs.defaults['deobfuscate_fonts'] = True
self.dedrmprefs.defaults['remove_watermarks'] = False
self.dedrmprefs.defaults['bandnkeys'] = {} self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {} self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {} self.dedrmprefs.defaults['ereaderkeys'] = {}
@@ -26,6 +28,7 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['androidkeys'] = {} self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = [] self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = [] self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['lcp_passphrases'] = []
self.dedrmprefs.defaults['adobewineprefix'] = "" self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = "" self.dedrmprefs.defaults['kindlewineprefix'] = ""
@@ -47,6 +50,8 @@ class DeDRM_Prefs():
self.dedrmprefs['pids'] = [] self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []: if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = [] self.dedrmprefs['serials'] = []
if self.dedrmprefs['lcp_passphrases'] == []:
self.dedrmprefs['lcp_passphrases'] = []
def __getitem__(self,kind = None): def __getitem__(self,kind = None):
if kind is not None: if kind is not None:

View File

@@ -20,7 +20,10 @@ class SimplePrefs(object):
self.file2key[filename] = key self.file2key[filename] = key
self.target = target + 'Prefs' self.target = target + 'Prefs'
if sys.platform.startswith('win'): 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\\") regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0] path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
prefdir = path + os.sep + self.target prefdir = path + os.sep + self.target

View File

@@ -32,11 +32,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data, str): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data) try:
self.stream.buffer.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -77,7 +83,7 @@ def unicode_argv():
return ["mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding or "utf-8" 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 #global switch
debug = False debug = False

View File

@@ -93,6 +93,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
# try finding winekeys anyway, even if above code errored # try finding winekeys anyway, even if above code errored
winekeys = [] winekeys = []
winekey_names = []
# get any files with extension in the output dir # get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)] files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files: for filename in files:
@@ -104,9 +105,10 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
else: else:
new_key_value = keyfile.read() new_key_value = keyfile.read()
winekeys.append(new_key_value) winekeys.append(new_key_value)
winekey_names.append(filename)
except: except:
print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename)) print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
traceback.print_exc() traceback.print_exc()
os.remove(fpath) 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")) 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

View File

@@ -209,6 +209,7 @@ def _EndRecData(fpin):
fpin.seek(-sizeEndCentDir, 2) fpin.seek(-sizeEndCentDir, 2)
except IOError: except IOError:
return None return None
data = fpin.read() data = fpin.read()
if data[0:4] == stringEndArchive and data[-2:] == "\000\000": if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
# the signature is correct and there's no comment, unpack structure # the signature is correct and there's no comment, unpack structure
@@ -662,7 +663,8 @@ class ZipFile:
self.comment = b'' self.comment = b''
# Check if we were passed a file-like object # 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._filePassed = 0
self.filename = file self.filename = file
modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'} modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}

View File

@@ -153,6 +153,7 @@ class fixZip:
nzinfo.internal_attr=zinfo.internal_attr nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system nzinfo.create_system=zinfo.create_system
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
self.outzip.writestr(nzinfo,data) self.outzip.writestr(nzinfo,data)
self.bzf.close() self.bzf.close()

View File

@@ -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***) ePubs (including Kobo and Google ePubs downloaded to ADE)
- Adobe Digital Editions (v2.0.1) PDFs - 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 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. - Once calibre has re-started, import the problem ebook.
- Now close calibre. - Now close calibre.
A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file. A log will appear that you can copy and paste into a GitHub issue 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.

39
FAQs.md
View File

@@ -21,18 +21,18 @@ But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the
### Recent Changes to Kindle for PC/Kindle for Mac ### Recent Changes to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac). Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
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. 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`: #### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
* MD-5: 53F793B562F4823721AA47D7DE099869 * MD-5: 53F793B562F4823721AA47D7DE099869
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255 * SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200 * SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
#### Kindle for Mac `KindleForMac-44182.dmg`: #### Kindle for Mac `KindleForMac-44182.dmg`:
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F * MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C * 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 may also need to take further action to prevent an auto update. The simplest wayis to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/ just delete the folder 'updates' and save a blank text file called 'updates' in its place. You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest 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.
@@ -51,13 +51,15 @@ Mac Note: If the chmod command fails with a permission error try again using `su
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading. After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
#### Decrypting KFX #### Decrypting KFX
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC version 1.26 or earlier (version later than 1.26 use a new encryption scheme for KFX files). In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread. Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
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
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead. Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
## Where can I get the latest version of these free DRM removal tools? ## Where can I get the latest version of these free DRM removal tools?
Right here at github. Just go to the [releases page](https://github.com/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? ## I've downloaded the tools archive. Now what?
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need. First, unzip the archive. You should now have a DeDRM folder containing several 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,7 +69,7 @@ Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin
# Installing the Tools # Installing the Tools
## The calibre plugin ## The calibre plugin
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u[path]DeDRM\_tools\_6.5.3.zip is invalid. It does not contain a top-level \_\_init\_\_.py file" ### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in 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. 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.
### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows. ### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows.
@@ -93,7 +95,7 @@ Your ebooks are stored on your computer or on your ebook reader. You need to fin
### Macintosh ### Macintosh
Navigating from your home folder, 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` 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 arent quite in the format the tools expect. To try to fix this: 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 arent quite in the format the tools expect. To try to fix this:
1. Deregister Kindle for PC(Mac) from your Amazon account. 1. Deregister Kindle for PC/Mac from your Amazon account.
1. Uninstall Kindle for PC(Mac) 1. Uninstall Kindle for PC/Mac
1. Delete the Kindle for PC(Mac) preferences 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` * 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) * 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. 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. Re-register Kindle for PC/Mac with your Amazon account
1. Download the ebook again. Do not use the files you have downloaded previously. 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. ## 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. There are several possible reasons why only some books get their DRM removed.
* You still dont have the DRM removal tools working correctly, but some of your books didnt have DRM in the first place. * You still dont have the DRM removal tools working correctly, but some of your books didnt 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. ## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully. Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
@@ -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. ## 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. 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? ## I cannot solve my problem with the DeDRM plugin, and now I need to post a log. How do I do that?
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository. Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a 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 # General Questions
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook? ## 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? ## 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. 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. 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? ## 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 books 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. Note these tools do NOT crack the DRM. They simply allow the books 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
## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work ## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive * Read the `ReadMe_Overview.txt` file in the top level of the tools archive
* Read the ReadMe file for the tool you want to use. * Read the ReadMe file for the tool you want to use.
* If you still cant 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 cant 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? ## Who wrote these scripts?
The authors tend to identify themselves only by pseudonyms: The authors tend to identify themselves only by pseudonyms:
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages * The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
* The Readium LCP support for this plugin was created by NoDRM
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser * The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle * The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
* The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others * The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others

View File

@@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '6.7.0' __version__ = '10.0.0'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
##################################################################### #####################################################################
@@ -20,7 +20,7 @@ except NameError:
PLUGIN_NAME = 'Obok DeDRM' PLUGIN_NAME = 'Obok DeDRM'
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_') PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.') PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
PLUGIN_VERSION_TUPLE = (6, 7, 0) PLUGIN_VERSION_TUPLE = (10, 0, 0)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm' HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
PLUGIN_AUTHORS = 'Anon' PLUGIN_AUTHORS = 'Anon'

View File

@@ -375,7 +375,6 @@ class InterfacePluginAction(InterfaceAction):
#print ('Kobo library filename: {0}'.format(book.filename)) #print ('Kobo library filename: {0}'.format(book.filename))
for userkey in self.userkeys: for userkey in self.userkeys:
print (_('Trying key: '), codecs.encode(userkey, 'hex')) print (_('Trying key: '), codecs.encode(userkey, 'hex'))
check = True
try: try:
fileout = PersistentTemporaryFile('.epub', dir=self.tdir) fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
#print ('Temp file: {0}'.format(fileout.name)) #print ('Temp file: {0}'.format(fileout.name))
@@ -396,8 +395,7 @@ class InterfacePluginAction(InterfaceAction):
file = book.encryptedfiles[filename] file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents) contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong. # Parse failures mean the key is probably wrong.
if check: file.check(contents)
check = not file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
zin.close() zin.close()

View File

@@ -204,7 +204,7 @@ class AddSerialDialog(QDialog):
def accept(self): def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace(): if len(self.key_name) == 0 or self.key_name.isspace():
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) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13: 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)) errmsg = "EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))

View File

@@ -42,7 +42,10 @@ class legacy_obok(object):
pwsdid = '' pwsdid = ''
try: try:
if sys.platform.startswith('win'): 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') regkey_browser = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
cookies = winreg.QueryValueEx(regkey_browser, 'cookies') cookies = winreg.QueryValueEx(regkey_browser, 'cookies')
bytearrays = cookies[0] bytearrays = cookies[0]

View File

@@ -1,6 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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 # Version 4.0.0 September 2020
# Python 3.0 # Python 3.0
# #
@@ -353,7 +360,10 @@ class KoboLibrary(object):
if (self.kobodir == u""): if (self.kobodir == u""):
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
if sys.getwindowsversion().major > 5: if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
@@ -365,9 +375,33 @@ class KoboLibrary(object):
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition") self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition") self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
#elif linux_path != None: elif sys.platform.startswith('linux'):
# 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") #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 # desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, "Kobo.sqlite") kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
# check for existence of file # check for existence of file
@@ -443,15 +477,22 @@ class KoboLibrary(object):
"""The list of all MAC addresses on this machine.""" """The list of all MAC addresses on this machine."""
macaddrs = [] macaddrs = []
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout try:
for line in output: output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
m = c.search(line) for line in output:
if m: m = c.search(line)
macaddrs.append(re.sub("-", ":", m.group(1)).upper()) 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'): elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
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) matches = c.findall(output)
for m in matches: for m in matches:
# print "m:{0}".format(m[0]) # print "m:{0}".format(m[0])

View File

@@ -8,7 +8,7 @@
<body> <body>
<h1>Obok DeDRM Plugin</h1> <h1>Obok DeDRM Plugin</h1>
<h3>(version 3.1.3)</h3> <h3>(version 10.0.0)</h3>
<h3>Installation:</h3> <h3>Installation:</h3>
@@ -22,7 +22,7 @@
<h3>Troubleshooting:</h3> <h3>Troubleshooting:</h3>
<p >If you find that its 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 its 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> <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.

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n" "POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2020-12-25 13:38+0100\n" "PO-Revision-Date: 2021-01-19 12:20+0100\n"
"Language: sv\n" "Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -41,8 +41,8 @@ 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 #: 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." msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr "" msgstr ""
"<p>Inga användarnycklar hittades för att dekryptera böcker med. Ingen idé " "<p>Inga användarnycklar hittades för att dekryptera böcker med. Det är ingen "
"att fortsätta." "idé att fortsätta."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user." msgid "{} - Decryption canceled by user."
@@ -87,13 +87,13 @@ msgstr "dubblett upptäcktes"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}" msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - EPUB-format har lagts till i 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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid "" msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen." "{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr "" msgstr ""
"{0} - Fel vid tilläggning av EPUB-format till befintligt {1}. Detta borde " "{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
"verkligen inte hända." "verkligen inte hända."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
@@ -118,8 +118,8 @@ msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be " " to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten." "overwritten."
msgstr "" msgstr ""
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er " " till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
"kommer att skrivas över." "att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid "" msgid ""
@@ -132,20 +132,19 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297 #: 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 />" msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr "" msgstr ""
"<p><b>{0}</b> -- lades inte till 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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid "" msgid ""
"Would you like to try and add the EPUB format to an available calibre " "Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />" "duplicate?<br /><br />"
msgstr "" msgstr ""
"Vill du försöka lägga till EPUB-format till en tillgänglig calibre-dubblett?" "Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
"<br /><br />" "<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten." msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr "OBS: inga befintliga EPUB:er kommer att skrivas över." msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: " msgid "Trying key: "
@@ -153,7 +152,7 @@ msgstr "Försöker med nyckel: "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key." msgid "Decryption failed, trying next key."
msgstr "Det gick inte att dekryptera, försöker med 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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.." msgid "Unknown Error decrypting, trying next key.."
@@ -164,8 +163,8 @@ msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into " "<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues." "existing calibre ebooks.<br /><br />No issues."
msgstr "" msgstr ""
"<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller " "<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
"infogats i befintliga calibre-e-böcker.<br /><br />Inga problem." "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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added." msgid "<p>{0} successfully added."
@@ -200,8 +199,7 @@ msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Bokimport avbröts av användaren:</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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid "" msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr "" msgstr ""
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n" "<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
@@ -209,8 +207,7 @@ msgstr ""
msgid "" msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n" "<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr "" msgstr ""
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br /" "<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
">\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid "" msgid ""
@@ -222,7 +219,7 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444 #: 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" msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Formatimporten 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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title" msgid "Unknown Book Title"
@@ -234,14 +231,11 @@ msgstr "den kunde inte dekrypteras."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid "" msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre " "user CHOSE not to insert the new EPUB format, or all existing calibre entries "
"entries HAD an EPUB format already." "HAD an EPUB format already."
msgstr "" msgstr ""
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla " "användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
"befintliga calibre-poster hade redan ett EPUB-format.\n" "calibre-poster hade redan ett EPUB-format."
"\n"
"användaren valde att inte infoga det nya EPUB-formatet, eller alla "
"befintliga kaliberposter hade redan ett EPUB-format."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!" msgid "of unknown reasons. Gosh I'm embarrassed!"
@@ -282,7 +276,7 @@ msgid ""
"cause calibre ebooks to be overwritten" "cause calibre ebooks to be overwritten"
msgstr "" msgstr ""
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att " "<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
"göra att calibre-e-böcker skrivs över" "orsaka att calibre-e-böcker skrivs över"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask" msgid "Ask"
@@ -363,7 +357,7 @@ msgstr "Felaktig AES-nyckel används"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key" msgid "Failed to initialize AES key"
msgstr "Misslyckades 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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed" msgid "AES decryption failed"

View File

@@ -129,7 +129,10 @@ if iswindows:
c_long, c_ulong c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL from ctypes.wintypes import LPVOID, DWORD, BOOL
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes.util import find_library from ctypes.util import find_library

View File

@@ -98,7 +98,10 @@ def getNookLogFiles():
logFiles = [] logFiles = []
found = False found = False
if iswindows: 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 # 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 # or the python interface to the 32 vs 64 bit registry is broken

View File

@@ -177,7 +177,10 @@ if iswindows:
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast string_at, Structure, c_void_p, cast
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
MAX_PATH = 255 MAX_PATH = 255
kernel32 = windll.kernel32 kernel32 = windll.kernel32
advapi32 = windll.advapi32 advapi32 = windll.advapi32

View File

@@ -346,7 +346,10 @@ class KoboLibrary(object):
if (self.kobodir == u""): if (self.kobodir == u""):
# step 4. we haven't found a device with serials, so try desktop apps # step 4. we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import winreg try:
import winreg
except ImportError:
import _winreg as winreg
if sys.getwindowsversion().major > 5: if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x

View File

@@ -2290,10 +2290,13 @@ class PDFDocument(object):
import win32api import win32api
import win32security import win32security
import win32file import win32file
import winreg
except: except:
raise ADEPTError('PyWin Extension (Win32API module) needed.\n'+\ raise ADEPTError('PyWin Extension (Win32API module) needed.\n'+\
'Download from http://sourceforge.net/projects/pywin32/files/ ') 'Download from http://sourceforge.net/projects/pywin32/files/ ')
try:
import winreg
except ImportError:
import _winreg as winreg
try: try:
v0 = win32api.GetVolumeInformation('C:\\') v0 = win32api.GetVolumeInformation('C:\\')
v1 = win32api.GetSystemInfo()[6] v1 = win32api.GetSystemInfo()[6]

View File

@@ -1,23 +1,26 @@
# [Guide] How to remove DRM
Refer to [Wiki Page](https://github.com/apprenticeharper/DeDRM_tools/wiki/Exactly-how-to-remove-DRM)
# DeDRM_tools # DeDRM_tools
DeDRM tools for ebooks DeDRM tools for ebooks
This is a fork of Apprentice Harper's version of the DeDRM tools. I've added some of the PRs that still haven't been merged, as well as added some more features / bugfixes myself.
Take a look at [the CHANGELOG](https://github.com/noDRM/DeDRM_tools/blob/master/CHANGELOG.md) to see a list of changes since the last version by Apprentice Harper (v7.2.1). This plugin will start with version v10.0.0 so there won't be conflicting / duplicate version numbers when Apprentice Harper's version is updated again.
The 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.
# Original README from Apprentice Harper
This is a repository that tracks all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This includes the tools from a time before Apprentice Alf had a blog, and continues through to when Apprentice Harper (with help) took over maintenance of the tools. This is a repository that tracks all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This includes the tools from a time before Apprentice Alf had a blog, and continues through to when Apprentice Harper (with help) took over maintenance of the tools.
The individual scripts are now released as two plugins for calibre: DeDRM and Obok. The individual scripts are now released as two plugins for calibre: DeDRM and Obok.
The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats. The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
The Obok plugin handles Kobo DRM. The Obok plugin handles Kobo DRM.
Users with calibre 5.x or later should use release 7.0.0b2 or later of the tools. 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.0 of the tools. Users with calibe 4.x or earlier should use release 6.8.x of the tools.
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.
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 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 DRM can only be removed from KFX format files downloaded with Kindle for PC/Mac 1.26 or earlier. Amazon changes the DRM for KFX files in Kindle for PC/Mac 1.27 and later. 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 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.

View File

@@ -26,7 +26,7 @@ The tools are provided in the form of plugins for calibre. Calibre is an open so
DeDRM plugin for calibre (Mac OS X, Windows) 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.0 of the plugins. 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. 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.
@@ -55,6 +55,7 @@ It may be possible to use the plugins on a Linux system, but no support is given
Credits Credits
------- -------
The original inept and ignoble scripts were by i♥cabbages 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 mobidedrm and erdr2pml scripts were by The Dark Reverser
The original topaz DRM removal script was by CMBDTC The original topaz DRM removal script was by CMBDTC
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson